v0.33.0 62a09f5

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
16const favoriteTagHydrationBatchSize = 900
17
18type favoriteRepo struct {
19	queries *sqlc.Queries
20}
21
22// NewFavoriteRepository creates a new SQLite-backed favorite repository.
23func NewFavoriteRepository(db *sql.DB) repository.FavoriteRepository {
24	return &favoriteRepo{queries: sqlc.New(db)}
25}
26
27func (r *favoriteRepo) Save(ctx context.Context, fav *entity.Favorite) error {
28	log := logging.FromContext(ctx)
29	log.Debug().Str("url", fav.URL).Int64("id", int64(fav.ID)).Msg("saving favorite")
30
31	if fav.ID > 0 {
32		shortcutKey := sql.NullInt64{}
33		if fav.ShortcutKey != nil {
34			shortcutKey = sql.NullInt64{Int64: int64(*fav.ShortcutKey), Valid: true}
35		}
36		return r.queries.UpdateFavorite(ctx, sqlc.UpdateFavoriteParams{
37			Title:       sql.NullString{String: fav.Title, Valid: fav.Title != ""},
38			FaviconUrl:  sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""},
39			ShortcutKey: shortcutKey,
40			ID:          int64(fav.ID),
41		})
42	}
43
44	row, err := r.queries.CreateFavorite(ctx, sqlc.CreateFavoriteParams{
45		Url:        fav.URL,
46		Title:      sql.NullString{String: fav.Title, Valid: fav.Title != ""},
47		FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""},
48	})
49	if err != nil {
50		return err
51	}
52	favoriteFromRow(favoriteRowFromCreate(row), fav)
53	return nil
54}
55
56func (r *favoriteRepo) FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) {
57	row, err := r.queries.GetFavoriteByID(ctx, int64(id))
58	if err != nil {
59		if errors.Is(err, sql.ErrNoRows) {
60			return nil, nil
61		}
62		return nil, err
63	}
64	fav := favoriteFromRow(favoriteRowFromGetByID(row), nil)
65	return fav, r.hydrateTags(ctx, []*entity.Favorite{fav})
66}
67
68func (r *favoriteRepo) FindByURL(ctx context.Context, url string) (*entity.Favorite, error) {
69	row, err := r.queries.GetFavoriteByURL(ctx, url)
70	if err != nil {
71		if errors.Is(err, sql.ErrNoRows) {
72			return nil, nil
73		}
74		return nil, err
75	}
76	fav := favoriteFromRow(favoriteRowFromGetByURL(row), nil)
77	return fav, r.hydrateTags(ctx, []*entity.Favorite{fav})
78}
79
80func (r *favoriteRepo) GetAll(ctx context.Context) ([]*entity.Favorite, error) {
81	rows, err := r.queries.GetAllFavorites(ctx)
82	if err != nil {
83		return nil, err
84	}
85	favorites := favoritesFromAllRows(rows)
86	return favorites, r.hydrateTags(ctx, favorites)
87}
88
89func (r *favoriteRepo) GetByTag(ctx context.Context, tagID entity.TagID) ([]*entity.Favorite, error) {
90	rows, err := r.queries.GetFavoritesByTag(ctx, int64(tagID))
91	if err != nil {
92		return nil, err
93	}
94	favorites := favoritesFromTagRows(rows)
95	return favorites, r.hydrateTags(ctx, favorites)
96}
97
98func (r *favoriteRepo) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {
99	row, err := r.queries.GetFavoriteByShortcut(ctx, sql.NullInt64{Int64: int64(key), Valid: true})
100	if err != nil {
101		if errors.Is(err, sql.ErrNoRows) {
102			return nil, nil
103		}
104		return nil, err
105	}
106	fav := favoriteFromRow(favoriteRowFromGetByShortcut(row), nil)
107	return fav, r.hydrateTags(ctx, []*entity.Favorite{fav})
108}
109
110func (r *favoriteRepo) UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error {
111	return r.queries.UpdateFavoritePosition(ctx, sqlc.UpdateFavoritePositionParams{
112		Position: int64(position),
113		ID:       int64(id),
114	})
115}
116
117func (r *favoriteRepo) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
118	var k sql.NullInt64
119	if key != nil {
120		k = sql.NullInt64{Int64: int64(*key), Valid: true}
121	}
122	return r.queries.SetFavoriteShortcut(ctx, sqlc.SetFavoriteShortcutParams{
123		ShortcutKey: k,
124		ID:          int64(id),
125	})
126}
127
128func (r *favoriteRepo) Delete(ctx context.Context, id entity.FavoriteID) error {
129	return r.queries.DeleteFavorite(ctx, int64(id))
130}
131
132func (r *favoriteRepo) hydrateTags(ctx context.Context, favorites []*entity.Favorite) error {
133	if len(favorites) == 0 {
134		return nil
135	}
136
137	ids := make([]int64, 0, len(favorites))
138	byID := make(map[entity.FavoriteID]*entity.Favorite, len(favorites))
139	for _, fav := range favorites {
140		if fav == nil {
141			continue
142		}
143		fav.Tags = []entity.Tag{}
144		ids = append(ids, int64(fav.ID))
145		byID[fav.ID] = fav
146	}
147	if len(ids) == 0 {
148		return nil
149	}
150
151	for start := 0; start < len(ids); start += favoriteTagHydrationBatchSize {
152		end := min(start+favoriteTagHydrationBatchSize, len(ids))
153		rows, err := r.queries.GetTagsForFavorites(ctx, ids[start:end])
154		if err != nil {
155			return err
156		}
157		for _, row := range rows {
158			fav := byID[entity.FavoriteID(row.FavoriteID)]
159			if fav == nil {
160				continue
161			}
162			fav.Tags = append(fav.Tags, *tagFromRow(row.FavoriteTag))
163		}
164	}
165	return nil
166}
167
168type favoriteRow struct {
169	ID          int64
170	URL         string
171	Title       sql.NullString
172	FaviconURL  sql.NullString
173	ShortcutKey sql.NullInt64
174	Position    int64
175	CreatedAt   sql.NullTime
176	UpdatedAt   sql.NullTime
177}
178
179func favoriteFromRow(row favoriteRow, target *entity.Favorite) *entity.Favorite {
180	fav := target
181	if fav == nil {
182		fav = &entity.Favorite{}
183	}
184	fav.ID = entity.FavoriteID(row.ID)
185	fav.URL = row.URL
186	fav.Title = row.Title.String
187	fav.FaviconURL = row.FaviconURL.String
188	fav.Position = int(row.Position)
189	fav.CreatedAt = row.CreatedAt.Time
190	fav.UpdatedAt = row.UpdatedAt.Time
191	fav.Tags = nil
192	if row.ShortcutKey.Valid {
193		key := int(row.ShortcutKey.Int64)
194		fav.ShortcutKey = &key
195	} else {
196		fav.ShortcutKey = nil
197	}
198	return fav
199}
200
201func favoriteRowFromCreate(row sqlc.CreateFavoriteRow) favoriteRow {
202	return favoriteRow{
203		ID: row.ID, URL: row.Url, Title: row.Title, FaviconURL: row.FaviconUrl,
204		ShortcutKey: row.ShortcutKey, Position: row.Position, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt,
205	}
206}
207
208func favoriteRowFromGetByID(row sqlc.GetFavoriteByIDRow) favoriteRow {
209	return favoriteRow{
210		ID: row.ID, URL: row.Url, Title: row.Title, FaviconURL: row.FaviconUrl,
211		ShortcutKey: row.ShortcutKey, Position: row.Position, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt,
212	}
213}
214
215func favoriteRowFromGetByURL(row sqlc.GetFavoriteByURLRow) favoriteRow {
216	return favoriteRow{
217		ID: row.ID, URL: row.Url, Title: row.Title, FaviconURL: row.FaviconUrl,
218		ShortcutKey: row.ShortcutKey, Position: row.Position, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt,
219	}
220}
221
222func favoriteRowFromGetByShortcut(row sqlc.GetFavoriteByShortcutRow) favoriteRow {
223	return favoriteRow{
224		ID: row.ID, URL: row.Url, Title: row.Title, FaviconURL: row.FaviconUrl,
225		ShortcutKey: row.ShortcutKey, Position: row.Position, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt,
226	}
227}
228
229func favoritesFromAllRows(rows []sqlc.GetAllFavoritesRow) []*entity.Favorite {
230	favorites := make([]*entity.Favorite, len(rows))
231	for i := range rows {
232		row := rows[i]
233		favorites[i] = favoriteFromRow(favoriteRow{
234			ID: row.ID, URL: row.Url, Title: row.Title, FaviconURL: row.FaviconUrl,
235			ShortcutKey: row.ShortcutKey, Position: row.Position, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt,
236		}, nil)
237	}
238	return favorites
239}
240
241func favoritesFromTagRows(rows []sqlc.GetFavoritesByTagRow) []*entity.Favorite {
242	favorites := make([]*entity.Favorite, len(rows))
243	for i := range rows {
244		row := rows[i]
245		favorites[i] = favoriteFromRow(favoriteRow{
246			ID: row.ID, URL: row.Url, Title: row.Title, FaviconURL: row.FaviconUrl,
247			ShortcutKey: row.ShortcutKey, Position: row.Position, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt,
248		}, nil)
249	}
250	return favorites
251}
252
253// ==================== Tag Repository ====================
254
255type tagRepo struct {
256	queries *sqlc.Queries
257}
258
259// NewTagRepository creates a new SQLite-backed tag repository.
260func NewTagRepository(db *sql.DB) repository.TagRepository {
261	return &tagRepo{queries: sqlc.New(db)}
262}
263
264func (r *tagRepo) Save(ctx context.Context, tag *entity.Tag) error {
265	log := logging.FromContext(ctx)
266	log.Debug().Str("name", tag.Name).Int64("id", int64(tag.ID)).Msg("saving tag")
267
268	if tag.ID > 0 {
269		return r.queries.UpdateTag(ctx, sqlc.UpdateTagParams{
270			Name:  tag.Name,
271			Color: tag.Color,
272			ID:    int64(tag.ID),
273		})
274	}
275
276	row, err := r.queries.CreateTag(ctx, sqlc.CreateTagParams{
277		Name:  tag.Name,
278		Color: tag.Color,
279	})
280	if err != nil {
281		return err
282	}
283	tag.ID = entity.TagID(row.ID)
284	return nil
285}
286
287func (r *tagRepo) FindByID(ctx context.Context, id entity.TagID) (*entity.Tag, error) {
288	row, err := r.queries.GetTagByID(ctx, int64(id))
289	if err != nil {
290		if errors.Is(err, sql.ErrNoRows) {
291			return nil, nil
292		}
293		return nil, err
294	}
295	return tagFromRow(row), nil
296}
297
298func (r *tagRepo) FindByName(ctx context.Context, name string) (*entity.Tag, error) {
299	row, err := r.queries.GetTagByName(ctx, name)
300	if err != nil {
301		if errors.Is(err, sql.ErrNoRows) {
302			return nil, nil
303		}
304		return nil, err
305	}
306	return tagFromRow(row), nil
307}
308
309func (r *tagRepo) GetAll(ctx context.Context) ([]*entity.Tag, error) {
310	rows, err := r.queries.GetAllTags(ctx)
311	if err != nil {
312		return nil, err
313	}
314	return tagsFromRows(rows), nil
315}
316
317func (r *tagRepo) AssignToFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error {
318	return r.queries.AssignTagToFavorite(ctx, sqlc.AssignTagToFavoriteParams{
319		FavoriteID: int64(favID),
320		TagID:      int64(tagID),
321	})
322}
323
324func (r *tagRepo) RemoveFromFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error {
325	return r.queries.RemoveTagFromFavorite(ctx, sqlc.RemoveTagFromFavoriteParams{
326		FavoriteID: int64(favID),
327		TagID:      int64(tagID),
328	})
329}
330
331func (r *tagRepo) GetForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) {
332	rows, err := r.queries.GetTagsForFavorite(ctx, int64(favID))
333	if err != nil {
334		return nil, err
335	}
336	return tagsFromRows(rows), nil
337}
338
339func (r *tagRepo) Delete(ctx context.Context, id entity.TagID) error {
340	return r.queries.DeleteTag(ctx, int64(id))
341}
342
343func tagFromRow(row sqlc.FavoriteTag) *entity.Tag {
344	return &entity.Tag{
345		ID:        entity.TagID(row.ID),
346		Name:      row.Name,
347		Color:     row.Color,
348		CreatedAt: row.CreatedAt.Time,
349	}
350}
351
352func tagsFromRows(rows []sqlc.FavoriteTag) []*entity.Tag {
353	tags := make([]*entity.Tag, len(rows))
354	for i, row := range rows {
355		tags[i] = tagFromRow(row)
356	}
357	return tags
358}

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