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}