1package sqlite
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7
8 "github.com/bnema/dumber/internal/domain/entity"
9 "github.com/bnema/dumber/internal/domain/repository"
10 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc"
11 "github.com/bnema/dumber/internal/logging"
12)
13
14// ==================== Favorite Repository ====================
15
16type favoriteRepo struct {
17 db *sql.DB
18 queries *sqlc.Queries
19}
20
21// NewFavoriteRepository creates a new SQLite-backed favorite repository.
22func NewFavoriteRepository(db *sql.DB) repository.FavoriteRepository {
23 return &favoriteRepo{db: db, queries: sqlc.New(db)}
24}
25
26func (r *favoriteRepo) Save(ctx context.Context, fav *entity.Favorite) error {
27 log := logging.FromContext(ctx)
28 log.Debug().Str("url", fav.URL).Int64("id", int64(fav.ID)).Msg("saving favorite")
29
30 var folderID sql.NullInt64
31 if fav.FolderID != nil {
32 folderID = sql.NullInt64{Int64: int64(*fav.FolderID), Valid: true}
33 }
34
35 if fav.ID > 0 {
36 return r.updateExisting(ctx, fav, folderID)
37 }
38
39 row, err := r.queries.CreateFavorite(ctx, sqlc.CreateFavoriteParams{
40 Url: fav.URL,
41 Title: sql.NullString{String: fav.Title, Valid: fav.Title != ""},
42 FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""},
43 FolderID: folderID,
44 })
45 if err != nil {
46 return err
47 }
48 fav.ID = entity.FavoriteID(row.ID)
49 return nil
50}
51
52func (r *favoriteRepo) updateExisting(ctx context.Context, fav *entity.Favorite, folderID sql.NullInt64) error {
53 log := logging.FromContext(ctx)
54 tx, err := r.db.BeginTx(ctx, nil)
55 if err != nil {
56 return err
57 }
58 defer func() {
59 if rollbackErr := tx.Rollback(); rollbackErr != nil && !errors.Is(rollbackErr, sql.ErrTxDone) {
60 log.Debug().Err(rollbackErr).Msg("favorite update rollback reported non-terminal error")
61 }
62 }()
63
64 shortcutKey := sql.NullInt64{}
65 if fav.ShortcutKey != nil {
66 shortcutKey = sql.NullInt64{Int64: int64(*fav.ShortcutKey), Valid: true}
67 }
68
69 queries := r.queries.WithTx(tx)
70 if err := queries.UpdateFavorite(ctx, sqlc.UpdateFavoriteParams{
71 Title: sql.NullString{String: fav.Title, Valid: fav.Title != ""},
72 FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""},
73 ID: int64(fav.ID),
74 }); err != nil {
75 return err
76 }
77 if err := queries.SetFavoriteFolder(ctx, sqlc.SetFavoriteFolderParams{FolderID: folderID, ID: int64(fav.ID)}); err != nil {
78 return err
79 }
80 if err := queries.SetFavoriteShortcut(ctx, sqlc.SetFavoriteShortcutParams{
81 ShortcutKey: shortcutKey,
82 ID: int64(fav.ID),
83 }); err != nil {
84 return err
85 }
86 return tx.Commit()
87}
88
89func (r *favoriteRepo) FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) {
90 row, err := r.queries.GetFavoriteByID(ctx, int64(id))
91 if err != nil {
92 if errors.Is(err, sql.ErrNoRows) {
93 return nil, nil
94 }
95 return nil, err
96 }
97 return favoriteFromRow(row), nil
98}
99
100func (r *favoriteRepo) FindByURL(ctx context.Context, url string) (*entity.Favorite, error) {
101 row, err := r.queries.GetFavoriteByURL(ctx, url)
102 if err != nil {
103 if errors.Is(err, sql.ErrNoRows) {
104 return nil, nil
105 }
106 return nil, err
107 }
108 return favoriteFromRow(row), nil
109}
110
111func (r *favoriteRepo) GetAll(ctx context.Context) ([]*entity.Favorite, error) {
112 rows, err := r.queries.GetAllFavorites(ctx)
113 if err != nil {
114 return nil, err
115 }
116 return favoritesFromRows(rows), nil
117}
118
119func (r *favoriteRepo) GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error) {
120 var rows []sqlc.Favorite
121 var err error
122
123 if folderID == nil {
124 rows, err = r.queries.GetFavoritesWithoutFolder(ctx)
125 } else {
126 rows, err = r.queries.GetFavoritesByFolder(ctx, sql.NullInt64{Int64: int64(*folderID), Valid: true})
127 }
128 if err != nil {
129 return nil, err
130 }
131 return favoritesFromRows(rows), nil
132}
133
134func (r *favoriteRepo) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {
135 row, err := r.queries.GetFavoriteByShortcut(ctx, sql.NullInt64{Int64: int64(key), Valid: true})
136 if err != nil {
137 if errors.Is(err, sql.ErrNoRows) {
138 return nil, nil
139 }
140 return nil, err
141 }
142 return favoriteFromRow(row), nil
143}
144
145func (r *favoriteRepo) UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error {
146 return r.queries.UpdateFavoritePosition(ctx, sqlc.UpdateFavoritePositionParams{
147 Position: int64(position),
148 ID: int64(id),
149 })
150}
151
152func (r *favoriteRepo) SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error {
153 var fid sql.NullInt64
154 if folderID != nil {
155 fid = sql.NullInt64{Int64: int64(*folderID), Valid: true}
156 }
157 return r.queries.SetFavoriteFolder(ctx, sqlc.SetFavoriteFolderParams{
158 FolderID: fid,
159 ID: int64(id),
160 })
161}
162
163func (r *favoriteRepo) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
164 var k sql.NullInt64
165 if key != nil {
166 k = sql.NullInt64{Int64: int64(*key), Valid: true}
167 }
168 return r.queries.SetFavoriteShortcut(ctx, sqlc.SetFavoriteShortcutParams{
169 ShortcutKey: k,
170 ID: int64(id),
171 })
172}
173
174func (r *favoriteRepo) Delete(ctx context.Context, id entity.FavoriteID) error {
175 return r.queries.DeleteFavorite(ctx, int64(id))
176}
177
178func favoriteFromRow(row sqlc.Favorite) *entity.Favorite {
179 fav := &entity.Favorite{
180 ID: entity.FavoriteID(row.ID),
181 URL: row.Url,
182 Title: row.Title.String,
183 FaviconURL: row.FaviconUrl.String,
184 Position: int(row.Position),
185 CreatedAt: row.CreatedAt.Time,
186 UpdatedAt: row.UpdatedAt.Time,
187 }
188 if row.FolderID.Valid {
189 fid := entity.FolderID(row.FolderID.Int64)
190 fav.FolderID = &fid
191 }
192 if row.ShortcutKey.Valid {
193 key := int(row.ShortcutKey.Int64)
194 fav.ShortcutKey = &key
195 }
196 return fav
197}
198
199func favoritesFromRows(rows []sqlc.Favorite) []*entity.Favorite {
200 favorites := make([]*entity.Favorite, len(rows))
201 for i := range rows {
202 favorites[i] = favoriteFromRow(rows[i])
203 }
204 return favorites
205}
206
207// ==================== Folder Repository ====================
208
209type folderRepo struct {
210 queries *sqlc.Queries
211}
212
213// NewFolderRepository creates a new SQLite-backed folder repository.
214func NewFolderRepository(db *sql.DB) repository.FolderRepository {
215 return &folderRepo{queries: sqlc.New(db)}
216}
217
218func (r *folderRepo) Save(ctx context.Context, folder *entity.Folder) error {
219 log := logging.FromContext(ctx)
220 log.Debug().Str("name", folder.Name).Int64("id", int64(folder.ID)).Msg("saving folder")
221
222 var parentID sql.NullInt64
223 if folder.ParentID != nil {
224 parentID = sql.NullInt64{Int64: int64(*folder.ParentID), Valid: true}
225 }
226
227 if folder.ID > 0 {
228 return r.queries.UpdateFolder(ctx, sqlc.UpdateFolderParams{
229 Name: folder.Name,
230 Icon: sql.NullString{String: folder.Icon, Valid: folder.Icon != ""},
231 ID: int64(folder.ID),
232 })
233 }
234
235 row, err := r.queries.CreateFolder(ctx, sqlc.CreateFolderParams{
236 Name: folder.Name,
237 Icon: sql.NullString{String: folder.Icon, Valid: folder.Icon != ""},
238 ParentID: parentID,
239 })
240 if err != nil {
241 return err
242 }
243 folder.ID = entity.FolderID(row.ID)
244 return nil
245}
246
247func (r *folderRepo) FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error) {
248 row, err := r.queries.GetFolderByID(ctx, int64(id))
249 if err != nil {
250 if errors.Is(err, sql.ErrNoRows) {
251 return nil, nil
252 }
253 return nil, err
254 }
255 return folderFromRow(row), nil
256}
257
258func (r *folderRepo) GetAll(ctx context.Context) ([]*entity.Folder, error) {
259 rows, err := r.queries.GetAllFolders(ctx)
260 if err != nil {
261 return nil, err
262 }
263 return foldersFromRows(rows), nil
264}
265
266func (r *folderRepo) GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error) {
267 var rows []sqlc.FavoriteFolder
268 var err error
269
270 if parentID == nil {
271 rows, err = r.queries.GetRootFolders(ctx)
272 } else {
273 rows, err = r.queries.GetChildFolders(ctx, sql.NullInt64{Int64: int64(*parentID), Valid: true})
274 }
275 if err != nil {
276 return nil, err
277 }
278 return foldersFromRows(rows), nil
279}
280
281func (r *folderRepo) UpdatePosition(ctx context.Context, id entity.FolderID, position int) error {
282 return r.queries.UpdateFolderPosition(ctx, sqlc.UpdateFolderPositionParams{
283 Position: int64(position),
284 ID: int64(id),
285 })
286}
287
288func (r *folderRepo) Delete(ctx context.Context, id entity.FolderID) error {
289 return r.queries.DeleteFolder(ctx, int64(id))
290}
291
292func folderFromRow(row sqlc.FavoriteFolder) *entity.Folder {
293 folder := &entity.Folder{
294 ID: entity.FolderID(row.ID),
295 Name: row.Name,
296 Icon: row.Icon.String,
297 Position: int(row.Position),
298 CreatedAt: row.CreatedAt.Time,
299 }
300 if row.ParentID.Valid {
301 pid := entity.FolderID(row.ParentID.Int64)
302 folder.ParentID = &pid
303 }
304 return folder
305}
306
307func foldersFromRows(rows []sqlc.FavoriteFolder) []*entity.Folder {
308 folders := make([]*entity.Folder, len(rows))
309 for i, row := range rows {
310 folders[i] = folderFromRow(row)
311 }
312 return folders
313}
314
315// ==================== Tag Repository ====================
316
317type tagRepo struct {
318 queries *sqlc.Queries
319}
320
321// NewTagRepository creates a new SQLite-backed tag repository.
322func NewTagRepository(db *sql.DB) repository.TagRepository {
323 return &tagRepo{queries: sqlc.New(db)}
324}
325
326func (r *tagRepo) Save(ctx context.Context, tag *entity.Tag) error {
327 log := logging.FromContext(ctx)
328 log.Debug().Str("name", tag.Name).Int64("id", int64(tag.ID)).Msg("saving tag")
329
330 if tag.ID > 0 {
331 return r.queries.UpdateTag(ctx, sqlc.UpdateTagParams{
332 Name: tag.Name,
333 Color: tag.Color,
334 ID: int64(tag.ID),
335 })
336 }
337
338 row, err := r.queries.CreateTag(ctx, sqlc.CreateTagParams{
339 Name: tag.Name,
340 Color: tag.Color,
341 })
342 if err != nil {
343 return err
344 }
345 tag.ID = entity.TagID(row.ID)
346 return nil
347}
348
349func (r *tagRepo) FindByID(ctx context.Context, id entity.TagID) (*entity.Tag, error) {
350 row, err := r.queries.GetTagByID(ctx, int64(id))
351 if err != nil {
352 if errors.Is(err, sql.ErrNoRows) {
353 return nil, nil
354 }
355 return nil, err
356 }
357 return tagFromRow(row), nil
358}
359
360func (r *tagRepo) FindByName(ctx context.Context, name string) (*entity.Tag, error) {
361 row, err := r.queries.GetTagByName(ctx, name)
362 if err != nil {
363 if errors.Is(err, sql.ErrNoRows) {
364 return nil, nil
365 }
366 return nil, err
367 }
368 return tagFromRow(row), nil
369}
370
371func (r *tagRepo) GetAll(ctx context.Context) ([]*entity.Tag, error) {
372 rows, err := r.queries.GetAllTags(ctx)
373 if err != nil {
374 return nil, err
375 }
376 return tagsFromRows(rows), nil
377}
378
379func (r *tagRepo) AssignToFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error {
380 return r.queries.AssignTagToFavorite(ctx, sqlc.AssignTagToFavoriteParams{
381 FavoriteID: int64(favID),
382 TagID: int64(tagID),
383 })
384}
385
386func (r *tagRepo) RemoveFromFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error {
387 return r.queries.RemoveTagFromFavorite(ctx, sqlc.RemoveTagFromFavoriteParams{
388 FavoriteID: int64(favID),
389 TagID: int64(tagID),
390 })
391}
392
393func (r *tagRepo) GetForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) {
394 rows, err := r.queries.GetTagsForFavorite(ctx, int64(favID))
395 if err != nil {
396 return nil, err
397 }
398 return tagsFromRows(rows), nil
399}
400
401func (r *tagRepo) Delete(ctx context.Context, id entity.TagID) error {
402 return r.queries.DeleteTag(ctx, int64(id))
403}
404
405func tagFromRow(row sqlc.FavoriteTag) *entity.Tag {
406 return &entity.Tag{
407 ID: entity.TagID(row.ID),
408 Name: row.Name,
409 Color: row.Color,
410 CreatedAt: row.CreatedAt.Time,
411 }
412}
413
414func tagsFromRows(rows []sqlc.FavoriteTag) []*entity.Tag {
415 tags := make([]*entity.Tag, len(rows))
416 for i, row := range rows {
417 tags[i] = tagFromRow(row)
418 }
419 return tags
420}