this repo has no description
1package db
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "time"
8
9 "tangled.org/core/appview/models"
10 "tangled.org/core/appview/pagination"
11)
12
13func (d *DB) CreateNotification(ctx context.Context, notification *models.Notification) error {
14 query := `
15 INSERT INTO notifications (recipient_did, actor_did, type, entity_type, entity_id, read, repo_id, issue_id, pull_id)
16 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
17 `
18
19 result, err := d.DB.ExecContext(ctx, query,
20 notification.RecipientDid,
21 notification.ActorDid,
22 string(notification.Type),
23 notification.EntityType,
24 notification.EntityId,
25 notification.Read,
26 notification.RepoId,
27 notification.IssueId,
28 notification.PullId,
29 )
30 if err != nil {
31 return fmt.Errorf("failed to create notification: %w", err)
32 }
33
34 id, err := result.LastInsertId()
35 if err != nil {
36 return fmt.Errorf("failed to get notification ID: %w", err)
37 }
38
39 notification.ID = id
40 return nil
41}
42
43// GetNotificationsPaginated retrieves notifications with filters and pagination
44func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...filter) ([]*models.Notification, error) {
45 var conditions []string
46 var args []any
47
48 for _, filter := range filters {
49 conditions = append(conditions, filter.Condition())
50 args = append(args, filter.Arg()...)
51 }
52
53 whereClause := ""
54 if len(conditions) > 0 {
55 whereClause = "WHERE " + conditions[0]
56 for _, condition := range conditions[1:] {
57 whereClause += " AND " + condition
58 }
59 }
60
61 query := fmt.Sprintf(`
62 select id, recipient_did, actor_did, type, entity_type, entity_id, read, created, repo_id, issue_id, pull_id
63 from notifications
64 %s
65 order by created desc
66 limit ? offset ?
67 `, whereClause)
68
69 args = append(args, page.Limit, page.Offset)
70
71 rows, err := e.QueryContext(context.Background(), query, args...)
72 if err != nil {
73 return nil, fmt.Errorf("failed to query notifications: %w", err)
74 }
75 defer rows.Close()
76
77 var notifications []*models.Notification
78 for rows.Next() {
79 var n models.Notification
80 var typeStr string
81 var createdStr string
82 err := rows.Scan(
83 &n.ID,
84 &n.RecipientDid,
85 &n.ActorDid,
86 &typeStr,
87 &n.EntityType,
88 &n.EntityId,
89 &n.Read,
90 &createdStr,
91 &n.RepoId,
92 &n.IssueId,
93 &n.PullId,
94 )
95 if err != nil {
96 return nil, fmt.Errorf("failed to scan notification: %w", err)
97 }
98 n.Type = models.NotificationType(typeStr)
99 n.Created, err = time.Parse(time.RFC3339, createdStr)
100 if err != nil {
101 return nil, fmt.Errorf("failed to parse created timestamp: %w", err)
102 }
103 notifications = append(notifications, &n)
104 }
105
106 return notifications, nil
107}
108
109// GetNotificationsWithEntities retrieves notifications with their related entities
110func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...filter) ([]*models.NotificationWithEntity, error) {
111 var conditions []string
112 var args []any
113
114 for _, filter := range filters {
115 conditions = append(conditions, filter.Condition())
116 args = append(args, filter.Arg()...)
117 }
118
119 whereClause := ""
120 if len(conditions) > 0 {
121 whereClause = "WHERE " + conditions[0]
122 for _, condition := range conditions[1:] {
123 whereClause += " AND " + condition
124 }
125 }
126
127 query := fmt.Sprintf(`
128 select
129 n.id, n.recipient_did, n.actor_did, n.type, n.entity_type, n.entity_id,
130 n.read, n.created, n.repo_id, n.issue_id, n.pull_id,
131 r.id as r_id, r.did as r_did, r.name as r_name, r.description as r_description,
132 i.id as i_id, i.did as i_did, i.issue_id as i_issue_id, i.title as i_title, i.open as i_open,
133 p.id as p_id, p.owner_did as p_owner_did, p.pull_id as p_pull_id, p.title as p_title, p.state as p_state
134 from notifications n
135 left join repos r on n.repo_id = r.id
136 left join issues i on n.issue_id = i.id
137 left join pulls p on n.pull_id = p.id
138 %s
139 order by n.created desc
140 limit ? offset ?
141 `, whereClause)
142
143 args = append(args, page.Limit, page.Offset)
144
145 rows, err := e.QueryContext(context.Background(), query, args...)
146 if err != nil {
147 return nil, fmt.Errorf("failed to query notifications with entities: %w", err)
148 }
149 defer rows.Close()
150
151 var notifications []*models.NotificationWithEntity
152 for rows.Next() {
153 var n models.Notification
154 var typeStr string
155 var createdStr string
156 var repo models.Repo
157 var issue models.Issue
158 var pull models.Pull
159 var rId, iId, pId sql.NullInt64
160 var rDid, rName, rDescription sql.NullString
161 var iDid sql.NullString
162 var iIssueId sql.NullInt64
163 var iTitle sql.NullString
164 var iOpen sql.NullBool
165 var pOwnerDid sql.NullString
166 var pPullId sql.NullInt64
167 var pTitle sql.NullString
168 var pState sql.NullInt64
169
170 err := rows.Scan(
171 &n.ID, &n.RecipientDid, &n.ActorDid, &typeStr, &n.EntityType, &n.EntityId,
172 &n.Read, &createdStr, &n.RepoId, &n.IssueId, &n.PullId,
173 &rId, &rDid, &rName, &rDescription,
174 &iId, &iDid, &iIssueId, &iTitle, &iOpen,
175 &pId, &pOwnerDid, &pPullId, &pTitle, &pState,
176 )
177 if err != nil {
178 return nil, fmt.Errorf("failed to scan notification with entities: %w", err)
179 }
180
181 n.Type = models.NotificationType(typeStr)
182 n.Created, err = time.Parse(time.RFC3339, createdStr)
183 if err != nil {
184 return nil, fmt.Errorf("failed to parse created timestamp: %w", err)
185 }
186
187 nwe := &models.NotificationWithEntity{Notification: &n}
188
189 // populate repo if present
190 if rId.Valid {
191 repo.Id = rId.Int64
192 if rDid.Valid {
193 repo.Did = rDid.String
194 }
195 if rName.Valid {
196 repo.Name = rName.String
197 }
198 if rDescription.Valid {
199 repo.Description = rDescription.String
200 }
201 nwe.Repo = &repo
202 }
203
204 // populate issue if present
205 if iId.Valid {
206 issue.Id = iId.Int64
207 if iDid.Valid {
208 issue.Did = iDid.String
209 }
210 if iIssueId.Valid {
211 issue.IssueId = int(iIssueId.Int64)
212 }
213 if iTitle.Valid {
214 issue.Title = iTitle.String
215 }
216 if iOpen.Valid {
217 issue.Open = iOpen.Bool
218 }
219 nwe.Issue = &issue
220 }
221
222 // populate pull if present
223 if pId.Valid {
224 pull.ID = int(pId.Int64)
225 if pOwnerDid.Valid {
226 pull.OwnerDid = pOwnerDid.String
227 }
228 if pPullId.Valid {
229 pull.PullId = int(pPullId.Int64)
230 }
231 if pTitle.Valid {
232 pull.Title = pTitle.String
233 }
234 if pState.Valid {
235 pull.State = models.PullState(pState.Int64)
236 }
237 nwe.Pull = &pull
238 }
239
240 notifications = append(notifications, nwe)
241 }
242
243 return notifications, nil
244}
245
246// GetNotifications retrieves notifications with filters
247func GetNotifications(e Execer, filters ...filter) ([]*models.Notification, error) {
248 return GetNotificationsPaginated(e, pagination.FirstPage(), filters...)
249}
250
251func (d *DB) GetUnreadNotificationCount(ctx context.Context, userDID string) (int, error) {
252 recipientFilter := FilterEq("recipient_did", userDID)
253 readFilter := FilterEq("read", 0)
254
255 query := fmt.Sprintf(`
256 SELECT COUNT(*)
257 FROM notifications
258 WHERE %s AND %s
259 `, recipientFilter.Condition(), readFilter.Condition())
260
261 args := append(recipientFilter.Arg(), readFilter.Arg()...)
262
263 var count int
264 err := d.DB.QueryRowContext(ctx, query, args...).Scan(&count)
265 if err != nil {
266 return 0, fmt.Errorf("failed to get unread count: %w", err)
267 }
268
269 return count, nil
270}
271
272func (d *DB) MarkNotificationRead(ctx context.Context, notificationID int64, userDID string) error {
273 idFilter := FilterEq("id", notificationID)
274 recipientFilter := FilterEq("recipient_did", userDID)
275
276 query := fmt.Sprintf(`
277 UPDATE notifications
278 SET read = 1
279 WHERE %s AND %s
280 `, idFilter.Condition(), recipientFilter.Condition())
281
282 args := append(idFilter.Arg(), recipientFilter.Arg()...)
283
284 result, err := d.DB.ExecContext(ctx, query, args...)
285 if err != nil {
286 return fmt.Errorf("failed to mark notification as read: %w", err)
287 }
288
289 rowsAffected, err := result.RowsAffected()
290 if err != nil {
291 return fmt.Errorf("failed to get rows affected: %w", err)
292 }
293
294 if rowsAffected == 0 {
295 return fmt.Errorf("notification not found or access denied")
296 }
297
298 return nil
299}
300
301func (d *DB) MarkAllNotificationsRead(ctx context.Context, userDID string) error {
302 recipientFilter := FilterEq("recipient_did", userDID)
303 readFilter := FilterEq("read", 0)
304
305 query := fmt.Sprintf(`
306 UPDATE notifications
307 SET read = 1
308 WHERE %s AND %s
309 `, recipientFilter.Condition(), readFilter.Condition())
310
311 args := append(recipientFilter.Arg(), readFilter.Arg()...)
312
313 _, err := d.DB.ExecContext(ctx, query, args...)
314 if err != nil {
315 return fmt.Errorf("failed to mark all notifications as read: %w", err)
316 }
317
318 return nil
319}
320
321func (d *DB) DeleteNotification(ctx context.Context, notificationID int64, userDID string) error {
322 idFilter := FilterEq("id", notificationID)
323 recipientFilter := FilterEq("recipient_did", userDID)
324
325 query := fmt.Sprintf(`
326 DELETE FROM notifications
327 WHERE %s AND %s
328 `, idFilter.Condition(), recipientFilter.Condition())
329
330 args := append(idFilter.Arg(), recipientFilter.Arg()...)
331
332 result, err := d.DB.ExecContext(ctx, query, args...)
333 if err != nil {
334 return fmt.Errorf("failed to delete notification: %w", err)
335 }
336
337 rowsAffected, err := result.RowsAffected()
338 if err != nil {
339 return fmt.Errorf("failed to get rows affected: %w", err)
340 }
341
342 if rowsAffected == 0 {
343 return fmt.Errorf("notification not found or access denied")
344 }
345
346 return nil
347}
348
349func (d *DB) GetNotificationPreferences(ctx context.Context, userDID string) (*models.NotificationPreferences, error) {
350 userFilter := FilterEq("user_did", userDID)
351
352 query := fmt.Sprintf(`
353 SELECT id, user_did, repo_starred, issue_created, issue_commented, pull_created,
354 pull_commented, followed, pull_merged, issue_closed, email_notifications
355 FROM notification_preferences
356 WHERE %s
357 `, userFilter.Condition())
358
359 var prefs models.NotificationPreferences
360 err := d.DB.QueryRowContext(ctx, query, userFilter.Arg()...).Scan(
361 &prefs.ID,
362 &prefs.UserDid,
363 &prefs.RepoStarred,
364 &prefs.IssueCreated,
365 &prefs.IssueCommented,
366 &prefs.PullCreated,
367 &prefs.PullCommented,
368 &prefs.Followed,
369 &prefs.PullMerged,
370 &prefs.IssueClosed,
371 &prefs.EmailNotifications,
372 )
373
374 if err != nil {
375 if err == sql.ErrNoRows {
376 return &models.NotificationPreferences{
377 UserDid: userDID,
378 RepoStarred: true,
379 IssueCreated: true,
380 IssueCommented: true,
381 PullCreated: true,
382 PullCommented: true,
383 Followed: true,
384 PullMerged: true,
385 IssueClosed: true,
386 EmailNotifications: false,
387 }, nil
388 }
389 return nil, fmt.Errorf("failed to get notification preferences: %w", err)
390 }
391
392 return &prefs, nil
393}
394
395func (d *DB) UpdateNotificationPreferences(ctx context.Context, prefs *models.NotificationPreferences) error {
396 query := `
397 INSERT OR REPLACE INTO notification_preferences
398 (user_did, repo_starred, issue_created, issue_commented, pull_created,
399 pull_commented, followed, pull_merged, issue_closed, email_notifications)
400 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
401 `
402
403 result, err := d.DB.ExecContext(ctx, query,
404 prefs.UserDid,
405 prefs.RepoStarred,
406 prefs.IssueCreated,
407 prefs.IssueCommented,
408 prefs.PullCreated,
409 prefs.PullCommented,
410 prefs.Followed,
411 prefs.PullMerged,
412 prefs.IssueClosed,
413 prefs.EmailNotifications,
414 )
415 if err != nil {
416 return fmt.Errorf("failed to update notification preferences: %w", err)
417 }
418
419 if prefs.ID == 0 {
420 id, err := result.LastInsertId()
421 if err != nil {
422 return fmt.Errorf("failed to get preferences ID: %w", err)
423 }
424 prefs.ID = id
425 }
426
427 return nil
428}
429
430func (d *DB) ClearOldNotifications(ctx context.Context, olderThan time.Duration) error {
431 cutoff := time.Now().Add(-olderThan)
432 createdFilter := FilterLte("created", cutoff)
433
434 query := fmt.Sprintf(`
435 DELETE FROM notifications
436 WHERE %s
437 `, createdFilter.Condition())
438
439 _, err := d.DB.ExecContext(ctx, query, createdFilter.Arg()...)
440 if err != nil {
441 return fmt.Errorf("failed to cleanup old notifications: %w", err)
442 }
443
444 return nil
445}