apps/bookmarks/db.go 5.7 K raw
1
package main
2
3
import (
4
	"database/sql"
5
	"errors"
6
	"strings"
7
	"time"
8
9
	"github.com/stevedylandev/andromeda/pkg/auth"
10
)
11
12
const schema = `
13
CREATE TABLE IF NOT EXISTS categories (
14
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
15
    short_id   TEXT NOT NULL UNIQUE,
16
    name       TEXT NOT NULL UNIQUE,
17
    position   INTEGER NOT NULL DEFAULT 0,
18
    created_at INTEGER NOT NULL
19
);
20
21
CREATE TABLE IF NOT EXISTS links (
22
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
23
    short_id     TEXT NOT NULL UNIQUE,
24
    title        TEXT NOT NULL,
25
    url          TEXT NOT NULL,
26
    favicon_url  TEXT,
27
    category_id  INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
28
    created_at   INTEGER NOT NULL
29
);
30
31
CREATE INDEX IF NOT EXISTS idx_links_category ON links(category_id, created_at DESC);
32
`
33
34
func listCategories(db *sql.DB) ([]Category, error) {
35
	rows, err := db.Query(`SELECT id, short_id, name, position FROM categories ORDER BY position ASC, name COLLATE NOCASE`)
36
	if err != nil {
37
		return nil, err
38
	}
39
	defer rows.Close()
40
	out := []Category{}
41
	for rows.Next() {
42
		var c Category
43
		if err := rows.Scan(&c.ID, &c.ShortID, &c.Name, &c.Position); err != nil {
44
			return nil, err
45
		}
46
		out = append(out, c)
47
	}
48
	return out, rows.Err()
49
}
50
51
func createCategory(db *sql.DB, name string) (*Category, error) {
52
	shortID, err := auth.GenerateShortID(10)
53
	if err != nil {
54
		return nil, err
55
	}
56
	now := time.Now().UTC().Unix()
57
	var nextPos int64
58
	if err := db.QueryRow(`SELECT COALESCE(MAX(position), 0) + 1 FROM categories`).Scan(&nextPos); err != nil {
59
		return nil, err
60
	}
61
	res, err := db.Exec(`INSERT INTO categories (short_id, name, position, created_at) VALUES (?, ?, ?, ?)`, shortID, name, nextPos, now)
62
	if err != nil {
63
		return nil, err
64
	}
65
	id, _ := res.LastInsertId()
66
	return &Category{ID: id, ShortID: shortID, Name: name, Position: nextPos}, nil
67
}
68
69
func deleteCategoryByShortID(db *sql.DB, shortID string) (bool, error) {
70
	res, err := db.Exec(`DELETE FROM categories WHERE short_id = ?`, shortID)
71
	if err != nil {
72
		return false, err
73
	}
74
	n, _ := res.RowsAffected()
75
	return n > 0, nil
76
}
77
78
func moveCategory(db *sql.DB, shortID string, direction int) (bool, error) {
79
	tx, err := db.Begin()
80
	if err != nil {
81
		return false, err
82
	}
83
	defer tx.Rollback()
84
85
	var curID, curPos int64
86
	err = tx.QueryRow(`SELECT id, position FROM categories WHERE short_id = ?`, shortID).Scan(&curID, &curPos)
87
	if errors.Is(err, sql.ErrNoRows) {
88
		return false, nil
89
	}
90
	if err != nil {
91
		return false, err
92
	}
93
94
	var nbID, nbPos int64
95
	if direction < 0 {
96
		err = tx.QueryRow(`SELECT id, position FROM categories WHERE position < ? ORDER BY position DESC LIMIT 1`, curPos).Scan(&nbID, &nbPos)
97
	} else {
98
		err = tx.QueryRow(`SELECT id, position FROM categories WHERE position > ? ORDER BY position ASC LIMIT 1`, curPos).Scan(&nbID, &nbPos)
99
	}
100
	if errors.Is(err, sql.ErrNoRows) {
101
		return false, nil
102
	}
103
	if err != nil {
104
		return false, err
105
	}
106
107
	if _, err := tx.Exec(`UPDATE categories SET position = ? WHERE id = ?`, nbPos, curID); err != nil {
108
		return false, err
109
	}
110
	if _, err := tx.Exec(`UPDATE categories SET position = ? WHERE id = ?`, curPos, nbID); err != nil {
111
		return false, err
112
	}
113
	if err := tx.Commit(); err != nil {
114
		return false, err
115
	}
116
	return true, nil
117
}
118
119
func getCategoryByName(db *sql.DB, name string) (*Category, error) {
120
	name = strings.TrimSpace(name)
121
	var c Category
122
	err := db.QueryRow(`SELECT id, short_id, name, position FROM categories WHERE name = ?`, name).Scan(&c.ID, &c.ShortID, &c.Name, &c.Position)
123
	if errors.Is(err, sql.ErrNoRows) {
124
		return nil, nil
125
	}
126
	if err != nil {
127
		return nil, err
128
	}
129
	return &c, nil
130
}
131
132
func listLinks(db *sql.DB) ([]Link, error) {
133
	rows, err := db.Query(`SELECT id, short_id, title, url, favicon_url, category_id, created_at FROM links ORDER BY created_at DESC`)
134
	if err != nil {
135
		return nil, err
136
	}
137
	defer rows.Close()
138
	out := []Link{}
139
	for rows.Next() {
140
		var l Link
141
		var fav sql.NullString
142
		if err := rows.Scan(&l.ID, &l.ShortID, &l.Title, &l.URL, &fav, &l.CategoryID, &l.CreatedAt); err != nil {
143
			return nil, err
144
		}
145
		if fav.Valid && fav.String != "" {
146
			s := fav.String
147
			l.FaviconURL = &s
148
		}
149
		out = append(out, l)
150
	}
151
	return out, rows.Err()
152
}
153
154
func createLink(db *sql.DB, title, url string, faviconURL *string, categoryID int64) (*Link, error) {
155
	shortID, err := auth.GenerateShortID(10)
156
	if err != nil {
157
		return nil, err
158
	}
159
	now := time.Now().UTC().Unix()
160
	var fav any
161
	if faviconURL != nil && *faviconURL != "" {
162
		fav = *faviconURL
163
	}
164
	res, err := db.Exec(`INSERT INTO links (short_id, title, url, favicon_url, category_id, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
165
		shortID, title, url, fav, categoryID, now)
166
	if err != nil {
167
		return nil, err
168
	}
169
	id, _ := res.LastInsertId()
170
	link := &Link{ID: id, ShortID: shortID, Title: title, URL: url, CategoryID: categoryID, CreatedAt: now}
171
	if faviconURL != nil && *faviconURL != "" {
172
		s := *faviconURL
173
		link.FaviconURL = &s
174
	}
175
	return link, nil
176
}
177
178
func listLinksMissingFavicon(db *sql.DB) ([]struct {
179
	ID  int64
180
	URL string
181
}, error) {
182
	rows, err := db.Query(`SELECT id, url FROM links WHERE favicon_url IS NULL OR favicon_url = ''`)
183
	if err != nil {
184
		return nil, err
185
	}
186
	defer rows.Close()
187
	var out []struct {
188
		ID  int64
189
		URL string
190
	}
191
	for rows.Next() {
192
		var r struct {
193
			ID  int64
194
			URL string
195
		}
196
		if err := rows.Scan(&r.ID, &r.URL); err != nil {
197
			return nil, err
198
		}
199
		out = append(out, r)
200
	}
201
	return out, rows.Err()
202
}
203
204
func updateLinkFavicon(db *sql.DB, id int64, favicon *string) error {
205
	var v any
206
	if favicon != nil && *favicon != "" {
207
		v = *favicon
208
	}
209
	_, err := db.Exec(`UPDATE links SET favicon_url = ? WHERE id = ?`, v, id)
210
	return err
211
}
212
213
func deleteLinkByShortID(db *sql.DB, shortID string) (bool, error) {
214
	res, err := db.Exec(`DELETE FROM links WHERE short_id = ?`, shortID)
215
	if err != nil {
216
		return false, err
217
	}
218
	n, _ := res.RowsAffected()
219
	return n > 0, nil
220
}