···11+package models
22+33+import (
44+ "encoding/json"
55+ "time"
66+)
77+88+// Model defines the common interface that all domain models must implement
99+type Model interface {
1010+ // GetID returns the primary key identifier
1111+ GetID() int64
1212+1313+ // SetID sets the primary key identifier
1414+ SetID(id int64)
1515+1616+ // GetTableName returns the database table name for this model
1717+ GetTableName() string
1818+1919+ // GetCreatedAt returns when the model was created
2020+ GetCreatedAt() time.Time
2121+2222+ // SetCreatedAt sets when the model was created
2323+ SetCreatedAt(t time.Time)
2424+2525+ // GetUpdatedAt returns when the model was last updated
2626+ GetUpdatedAt() time.Time
2727+2828+ // SetUpdatedAt sets when the model was last updated
2929+ SetUpdatedAt(t time.Time)
3030+}
3131+3232+// Task represents a task item with TaskWarrior-inspired fields
3333+type Task struct {
3434+ ID int64 `json:"id"`
3535+ UUID string `json:"uuid"`
3636+ Description string `json:"description"`
3737+ // pending, completed, deleted
3838+ Status string `json:"status"`
3939+ // A-Z or empty
4040+ Priority string `json:"priority,omitempty"`
4141+ Project string `json:"project,omitempty"`
4242+ Tags []string `json:"tags,omitempty"`
4343+ Due *time.Time `json:"due,omitempty"`
4444+ Entry time.Time `json:"entry"`
4545+ Modified time.Time `json:"modified"`
4646+ // completion time
4747+ End *time.Time `json:"end,omitempty"`
4848+ // when task was started
4949+ Start *time.Time `json:"start,omitempty"`
5050+ Annotations []string `json:"annotations,omitempty"`
5151+}
5252+5353+// Movie represents a movie in the watch queue
5454+type Movie struct {
5555+ ID int64 `json:"id"`
5656+ Title string `json:"title"`
5757+ Year int `json:"year,omitempty"`
5858+ // queued, watched, removed
5959+ Status string `json:"status"`
6060+ Rating float64 `json:"rating,omitempty"`
6161+ Notes string `json:"notes,omitempty"`
6262+ Added time.Time `json:"added"`
6363+ Watched *time.Time `json:"watched,omitempty"`
6464+}
6565+6666+// TVShow represents a TV show in the watch queue
6767+type TVShow struct {
6868+ ID int64 `json:"id"`
6969+ Title string `json:"title"`
7070+ Season int `json:"season,omitempty"`
7171+ Episode int `json:"episode,omitempty"`
7272+ // queued, watching, watched, removed
7373+ Status string `json:"status"`
7474+ Rating float64 `json:"rating,omitempty"`
7575+ Notes string `json:"notes,omitempty"`
7676+ Added time.Time `json:"added"`
7777+ LastWatched *time.Time `json:"last_watched,omitempty"`
7878+}
7979+8080+// Book represents a book in the reading list
8181+type Book struct {
8282+ ID int64 `json:"id"`
8383+ Title string `json:"title"`
8484+ Author string `json:"author,omitempty"`
8585+ // queued, reading, finished, removed
8686+ Status string `json:"status"`
8787+ // percentage 0-100
8888+ Progress int `json:"progress"`
8989+ Pages int `json:"pages,omitempty"`
9090+ Rating float64 `json:"rating,omitempty"`
9191+ Notes string `json:"notes,omitempty"`
9292+ Added time.Time `json:"added"`
9393+ Started *time.Time `json:"started,omitempty"`
9494+ Finished *time.Time `json:"finished,omitempty"`
9595+}
9696+9797+// MarshalTags converts tags slice to JSON string for database storage
9898+func (t *Task) MarshalTags() (string, error) {
9999+ if len(t.Tags) == 0 {
100100+ return "", nil
101101+ }
102102+ data, err := json.Marshal(t.Tags)
103103+ return string(data), err
104104+}
105105+106106+// UnmarshalTags converts JSON string from database to tags slice
107107+func (t *Task) UnmarshalTags(data string) error {
108108+ if data == "" {
109109+ t.Tags = nil
110110+ return nil
111111+ }
112112+ return json.Unmarshal([]byte(data), &t.Tags)
113113+}
114114+115115+// MarshalAnnotations converts annotations slice to JSON string for database storage
116116+func (t *Task) MarshalAnnotations() (string, error) {
117117+ if len(t.Annotations) == 0 {
118118+ return "", nil
119119+ }
120120+ data, err := json.Marshal(t.Annotations)
121121+ return string(data), err
122122+}
123123+124124+// UnmarshalAnnotations converts JSON string from database to annotations slice
125125+func (t *Task) UnmarshalAnnotations(data string) error {
126126+ if data == "" {
127127+ t.Annotations = nil
128128+ return nil
129129+ }
130130+ return json.Unmarshal([]byte(data), &t.Annotations)
131131+}
132132+133133+// IsCompleted returns true if the task is marked as completed
134134+func (t *Task) IsCompleted() bool {
135135+ return t.Status == "completed"
136136+}
137137+138138+// IsPending returns true if the task is pending
139139+func (t *Task) IsPending() bool {
140140+ return t.Status == "pending"
141141+}
142142+143143+// IsDeleted returns true if the task is deleted
144144+func (t *Task) IsDeleted() bool {
145145+ return t.Status == "deleted"
146146+}
147147+148148+// HasPriority returns true if the task has a priority set
149149+func (t *Task) HasPriority() bool {
150150+ return t.Priority != ""
151151+}
152152+153153+// IsWatched returns true if the movie has been watched
154154+func (m *Movie) IsWatched() bool {
155155+ return m.Status == "watched"
156156+}
157157+158158+// IsQueued returns true if the movie is in the queue
159159+func (m *Movie) IsQueued() bool {
160160+ return m.Status == "queued"
161161+}
162162+163163+// IsWatching returns true if the TV show is currently being watched
164164+func (tv *TVShow) IsWatching() bool {
165165+ return tv.Status == "watching"
166166+}
167167+168168+// IsWatched returns true if the TV show has been watched
169169+func (tv *TVShow) IsWatched() bool {
170170+ return tv.Status == "watched"
171171+}
172172+173173+// IsQueued returns true if the TV show is in the queue
174174+func (tv *TVShow) IsQueued() bool {
175175+ return tv.Status == "queued"
176176+}
177177+178178+// IsReading returns true if the book is currently being read
179179+func (b *Book) IsReading() bool {
180180+ return b.Status == "reading"
181181+}
182182+183183+// IsFinished returns true if the book has been finished
184184+func (b *Book) IsFinished() bool {
185185+ return b.Status == "finished"
186186+}
187187+188188+// IsQueued returns true if the book is in the queue
189189+func (b *Book) IsQueued() bool {
190190+ return b.Status == "queued"
191191+}
192192+193193+// ProgressPercent returns the reading progress as a percentage
194194+func (b *Book) ProgressPercent() int {
195195+ return b.Progress
196196+}
197197+198198+func (t *Task) GetID() int64 { return t.ID }
199199+func (t *Task) SetID(id int64) { t.ID = id }
200200+func (t *Task) GetTableName() string { return "tasks" }
201201+func (t *Task) GetCreatedAt() time.Time { return t.Entry }
202202+func (t *Task) SetCreatedAt(time time.Time) { t.Entry = time }
203203+func (t *Task) GetUpdatedAt() time.Time { return t.Modified }
204204+func (t *Task) SetUpdatedAt(time time.Time) { t.Modified = time }
205205+206206+func (m *Movie) GetID() int64 { return m.ID }
207207+func (m *Movie) SetID(id int64) { m.ID = id }
208208+func (m *Movie) GetTableName() string { return "movies" }
209209+func (m *Movie) GetCreatedAt() time.Time { return m.Added }
210210+func (m *Movie) SetCreatedAt(time time.Time) { m.Added = time }
211211+func (m *Movie) GetUpdatedAt() time.Time { return m.Added }
212212+func (m *Movie) SetUpdatedAt(time time.Time) { m.Added = time }
213213+214214+func (tv *TVShow) GetID() int64 { return tv.ID }
215215+func (tv *TVShow) SetID(id int64) { tv.ID = id }
216216+func (tv *TVShow) GetTableName() string { return "tv_shows" }
217217+func (tv *TVShow) GetCreatedAt() time.Time { return tv.Added }
218218+func (tv *TVShow) SetCreatedAt(time time.Time) { tv.Added = time }
219219+func (tv *TVShow) GetUpdatedAt() time.Time { return tv.Added }
220220+func (tv *TVShow) SetUpdatedAt(time time.Time) { tv.Added = time }
221221+222222+func (b *Book) GetID() int64 { return b.ID }
223223+func (b *Book) SetID(id int64) { b.ID = id }
224224+func (b *Book) GetTableName() string { return "books" }
225225+func (b *Book) GetCreatedAt() time.Time { return b.Added }
226226+func (b *Book) SetCreatedAt(time time.Time) { b.Added = time }
227227+func (b *Book) GetUpdatedAt() time.Time { return b.Added }
228228+func (b *Book) SetUpdatedAt(time time.Time) { b.Added = time }
-164
internal/models/task.go
···11-package models
22-33-import (
44- "encoding/json"
55- "time"
66-)
77-88-// Task represents a task item with TaskWarrior-inspired fields
99-type Task struct {
1010- ID int64 `json:"id"`
1111- UUID string `json:"uuid"`
1212- Description string `json:"description"`
1313- Status string `json:"status"` // pending, completed, deleted
1414- Priority string `json:"priority,omitempty"` // A-Z or empty
1515- Project string `json:"project,omitempty"`
1616- Tags []string `json:"tags,omitempty"`
1717- Due *time.Time `json:"due,omitempty"`
1818- Entry time.Time `json:"entry"`
1919- Modified time.Time `json:"modified"`
2020- End *time.Time `json:"end,omitempty"` // completion time
2121- Start *time.Time `json:"start,omitempty"` // when task was started
2222- Annotations []string `json:"annotations,omitempty"`
2323-}
2424-2525-// Movie represents a movie in the watch queue
2626-type Movie struct {
2727- ID int64 `json:"id"`
2828- Title string `json:"title"`
2929- Year int `json:"year,omitempty"`
3030- Status string `json:"status"` // queued, watched, removed
3131- Rating float64 `json:"rating,omitempty"`
3232- Notes string `json:"notes,omitempty"`
3333- Added time.Time `json:"added"`
3434- Watched *time.Time `json:"watched,omitempty"`
3535-}
3636-3737-// TVShow represents a TV show in the watch queue
3838-type TVShow struct {
3939- ID int64 `json:"id"`
4040- Title string `json:"title"`
4141- Season int `json:"season,omitempty"`
4242- Episode int `json:"episode,omitempty"`
4343- Status string `json:"status"` // queued, watching, watched, removed
4444- Rating float64 `json:"rating,omitempty"`
4545- Notes string `json:"notes,omitempty"`
4646- Added time.Time `json:"added"`
4747- LastWatched *time.Time `json:"last_watched,omitempty"`
4848-}
4949-5050-// Book represents a book in the reading list
5151-type Book struct {
5252- ID int64 `json:"id"`
5353- Title string `json:"title"`
5454- Author string `json:"author,omitempty"`
5555- Status string `json:"status"` // queued, reading, finished, removed
5656- Progress int `json:"progress"` // percentage 0-100
5757- Pages int `json:"pages,omitempty"`
5858- Rating float64 `json:"rating,omitempty"`
5959- Notes string `json:"notes,omitempty"`
6060- Added time.Time `json:"added"`
6161- Started *time.Time `json:"started,omitempty"`
6262- Finished *time.Time `json:"finished,omitempty"`
6363-}
6464-6565-// MarshalTags converts tags slice to JSON string for database storage
6666-func (t *Task) MarshalTags() (string, error) {
6767- if len(t.Tags) == 0 {
6868- return "", nil
6969- }
7070- data, err := json.Marshal(t.Tags)
7171- return string(data), err
7272-}
7373-7474-// UnmarshalTags converts JSON string from database to tags slice
7575-func (t *Task) UnmarshalTags(data string) error {
7676- if data == "" {
7777- t.Tags = nil
7878- return nil
7979- }
8080- return json.Unmarshal([]byte(data), &t.Tags)
8181-}
8282-8383-// MarshalAnnotations converts annotations slice to JSON string for database storage
8484-func (t *Task) MarshalAnnotations() (string, error) {
8585- if len(t.Annotations) == 0 {
8686- return "", nil
8787- }
8888- data, err := json.Marshal(t.Annotations)
8989- return string(data), err
9090-}
9191-9292-// UnmarshalAnnotations converts JSON string from database to annotations slice
9393-func (t *Task) UnmarshalAnnotations(data string) error {
9494- if data == "" {
9595- t.Annotations = nil
9696- return nil
9797- }
9898- return json.Unmarshal([]byte(data), &t.Annotations)
9999-}
100100-101101-// IsCompleted returns true if the task is marked as completed
102102-func (t *Task) IsCompleted() bool {
103103- return t.Status == "completed"
104104-}
105105-106106-// IsPending returns true if the task is pending
107107-func (t *Task) IsPending() bool {
108108- return t.Status == "pending"
109109-}
110110-111111-// IsDeleted returns true if the task is deleted
112112-func (t *Task) IsDeleted() bool {
113113- return t.Status == "deleted"
114114-}
115115-116116-// HasPriority returns true if the task has a priority set
117117-func (t *Task) HasPriority() bool {
118118- return t.Priority != ""
119119-}
120120-121121-// IsWatched returns true if the movie has been watched
122122-func (m *Movie) IsWatched() bool {
123123- return m.Status == "watched"
124124-}
125125-126126-// IsQueued returns true if the movie is in the queue
127127-func (m *Movie) IsQueued() bool {
128128- return m.Status == "queued"
129129-}
130130-131131-// IsWatching returns true if the TV show is currently being watched
132132-func (tv *TVShow) IsWatching() bool {
133133- return tv.Status == "watching"
134134-}
135135-136136-// IsWatched returns true if the TV show has been watched
137137-func (tv *TVShow) IsWatched() bool {
138138- return tv.Status == "watched"
139139-}
140140-141141-// IsQueued returns true if the TV show is in the queue
142142-func (tv *TVShow) IsQueued() bool {
143143- return tv.Status == "queued"
144144-}
145145-146146-// IsReading returns true if the book is currently being read
147147-func (b *Book) IsReading() bool {
148148- return b.Status == "reading"
149149-}
150150-151151-// IsFinished returns true if the book has been finished
152152-func (b *Book) IsFinished() bool {
153153- return b.Status == "finished"
154154-}
155155-156156-// IsQueued returns true if the book is in the queue
157157-func (b *Book) IsQueued() bool {
158158- return b.Status == "queued"
159159-}
160160-161161-// ProgressPercent returns the reading progress as a percentage
162162-func (b *Book) ProgressPercent() int {
163163- return b.Progress
164164-}
+53
internal/repo/repo.go
···11+package repo
22+33+import (
44+ "context"
55+66+ "stormlightlabs.org/noteleaf/internal/models"
77+)
88+99+// Repository defines a general, behavior-focused interface for data access
1010+type Repository interface {
1111+ // Create stores a new model and returns its assigned ID
1212+ Create(ctx context.Context, model models.Model) (int64, error)
1313+1414+ // Get retrieves a model by ID
1515+ Get(ctx context.Context, table string, id int64, dest models.Model) error
1616+1717+ // Update modifies an existing model
1818+ Update(ctx context.Context, model models.Model) error
1919+2020+ // Delete removes a model by ID
2121+ Delete(ctx context.Context, table string, id int64) error
2222+2323+ // List retrieves models with optional filtering and sorting
2424+ List(ctx context.Context, table string, opts ListOptions, dest any) error
2525+2626+ // Find retrieves models matching specific conditions
2727+ Find(ctx context.Context, table string, conditions map[string]any, dest any) error
2828+2929+ // Count returns the number of models matching conditions
3030+ Count(ctx context.Context, table string, conditions map[string]any) (int64, error)
3131+3232+ // Execute runs a custom query with parameters
3333+ Execute(ctx context.Context, query string, args ...any) error
3434+3535+ // Query runs a custom query and returns results
3636+ Query(ctx context.Context, query string, dest any, args ...any) error
3737+}
3838+3939+// ListOptions defines generic options for listing items
4040+type ListOptions struct {
4141+ // field: value pairs for WHERE conditions
4242+ Where map[string]any
4343+ Limit int
4444+ Offset int
4545+ // field name to sort by
4646+ SortBy string
4747+ // "asc" or "desc"
4848+ SortOrder string
4949+ // general search term
5050+ Search string
5151+ // fields to search in
5252+ SearchFields []string
5353+}