v0.30.0 eb3068f

favorite_repo.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close