···
9
+
"tangled.org/core/appview/models"
10
+
"tangled.org/core/appview/pagination"
13
+
func (d *DB) CreateNotification(ctx context.Context, notification *models.Notification) error {
15
+
INSERT INTO notifications (recipient_did, actor_did, type, entity_type, entity_id, read, repo_id, issue_id, pull_id)
16
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
19
+
result, err := d.DB.ExecContext(ctx, query,
20
+
notification.RecipientDid,
21
+
notification.ActorDid,
22
+
string(notification.Type),
23
+
notification.EntityType,
24
+
notification.EntityId,
26
+
notification.RepoId,
27
+
notification.IssueId,
28
+
notification.PullId,
31
+
return fmt.Errorf("failed to create notification: %w", err)
34
+
id, err := result.LastInsertId()
36
+
return fmt.Errorf("failed to get notification ID: %w", err)
39
+
notification.ID = id
43
+
// GetNotificationsPaginated retrieves notifications with filters and pagination
44
+
func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...filter) ([]*models.Notification, error) {
45
+
var conditions []string
48
+
for _, filter := range filters {
49
+
conditions = append(conditions, filter.Condition())
50
+
args = append(args, filter.Arg()...)
54
+
if len(conditions) > 0 {
55
+
whereClause = "WHERE " + conditions[0]
56
+
for _, condition := range conditions[1:] {
57
+
whereClause += " AND " + condition
61
+
query := fmt.Sprintf(`
62
+
select id, recipient_did, actor_did, type, entity_type, entity_id, read, created, repo_id, issue_id, pull_id
65
+
order by created desc
69
+
args = append(args, page.Limit, page.Offset)
71
+
rows, err := e.QueryContext(context.Background(), query, args...)
73
+
return nil, fmt.Errorf("failed to query notifications: %w", err)
77
+
var notifications []*models.Notification
79
+
var n models.Notification
81
+
var createdStr string
96
+
return nil, fmt.Errorf("failed to scan notification: %w", err)
98
+
n.Type = models.NotificationType(typeStr)
99
+
n.Created, err = time.Parse(time.RFC3339, createdStr)
101
+
return nil, fmt.Errorf("failed to parse created timestamp: %w", err)
103
+
notifications = append(notifications, &n)
106
+
return notifications, nil
109
+
// GetNotificationsWithEntities retrieves notifications with their related entities
110
+
func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...filter) ([]*models.NotificationWithEntity, error) {
111
+
var conditions []string
114
+
for _, filter := range filters {
115
+
conditions = append(conditions, filter.Condition())
116
+
args = append(args, filter.Arg()...)
120
+
if len(conditions) > 0 {
121
+
whereClause = "WHERE " + conditions[0]
122
+
for _, condition := range conditions[1:] {
123
+
whereClause += " AND " + condition
127
+
query := fmt.Sprintf(`
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
139
+
order by n.created desc
143
+
args = append(args, page.Limit, page.Offset)
145
+
rows, err := e.QueryContext(context.Background(), query, args...)
147
+
return nil, fmt.Errorf("failed to query notifications with entities: %w", err)
151
+
var notifications []*models.NotificationWithEntity
153
+
var n models.Notification
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
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,
178
+
return nil, fmt.Errorf("failed to scan notification with entities: %w", err)
181
+
n.Type = models.NotificationType(typeStr)
182
+
n.Created, err = time.Parse(time.RFC3339, createdStr)
184
+
return nil, fmt.Errorf("failed to parse created timestamp: %w", err)
187
+
nwe := &models.NotificationWithEntity{Notification: &n}
189
+
// populate repo if present
191
+
repo.Id = rId.Int64
193
+
repo.Did = rDid.String
196
+
repo.Name = rName.String
198
+
if rDescription.Valid {
199
+
repo.Description = rDescription.String
204
+
// populate issue if present
206
+
issue.Id = iId.Int64
208
+
issue.Did = iDid.String
210
+
if iIssueId.Valid {
211
+
issue.IssueId = int(iIssueId.Int64)
214
+
issue.Title = iTitle.String
217
+
issue.Open = iOpen.Bool
222
+
// populate pull if present
224
+
pull.ID = int(pId.Int64)
225
+
if pOwnerDid.Valid {
226
+
pull.OwnerDid = pOwnerDid.String
229
+
pull.PullId = int(pPullId.Int64)
232
+
pull.Title = pTitle.String
235
+
pull.State = models.PullState(pState.Int64)
240
+
notifications = append(notifications, nwe)
243
+
return notifications, nil
246
+
// GetNotifications retrieves notifications with filters
247
+
func GetNotifications(e Execer, filters ...filter) ([]*models.Notification, error) {
248
+
return GetNotificationsPaginated(e, pagination.FirstPage(), filters...)
251
+
// GetNotifications retrieves notifications for a user with pagination (legacy method for backward compatibility)
252
+
func (d *DB) GetNotifications(ctx context.Context, userDID string, limit, offset int) ([]*models.Notification, error) {
253
+
page := pagination.Page{Limit: limit, Offset: offset}
254
+
return GetNotificationsPaginated(d.DB, page, FilterEq("recipient_did", userDID))
257
+
// GetNotificationsWithEntities retrieves notifications with entities for a user with pagination
258
+
func (d *DB) GetNotificationsWithEntities(ctx context.Context, userDID string, limit, offset int) ([]*models.NotificationWithEntity, error) {
259
+
page := pagination.Page{Limit: limit, Offset: offset}
260
+
return GetNotificationsWithEntities(d.DB, page, FilterEq("recipient_did", userDID))
263
+
func (d *DB) GetUnreadNotificationCount(ctx context.Context, userDID string) (int, error) {
264
+
recipientFilter := FilterEq("recipient_did", userDID)
265
+
readFilter := FilterEq("read", 0)
267
+
query := fmt.Sprintf(`
271
+
`, recipientFilter.Condition(), readFilter.Condition())
273
+
args := append(recipientFilter.Arg(), readFilter.Arg()...)
276
+
err := d.DB.QueryRowContext(ctx, query, args...).Scan(&count)
278
+
return 0, fmt.Errorf("failed to get unread count: %w", err)
284
+
func (d *DB) MarkNotificationRead(ctx context.Context, notificationID int64, userDID string) error {
285
+
idFilter := FilterEq("id", notificationID)
286
+
recipientFilter := FilterEq("recipient_did", userDID)
288
+
query := fmt.Sprintf(`
289
+
UPDATE notifications
292
+
`, idFilter.Condition(), recipientFilter.Condition())
294
+
args := append(idFilter.Arg(), recipientFilter.Arg()...)
296
+
result, err := d.DB.ExecContext(ctx, query, args...)
298
+
return fmt.Errorf("failed to mark notification as read: %w", err)
301
+
rowsAffected, err := result.RowsAffected()
303
+
return fmt.Errorf("failed to get rows affected: %w", err)
306
+
if rowsAffected == 0 {
307
+
return fmt.Errorf("notification not found or access denied")
313
+
func (d *DB) MarkAllNotificationsRead(ctx context.Context, userDID string) error {
314
+
recipientFilter := FilterEq("recipient_did", userDID)
315
+
readFilter := FilterEq("read", 0)
317
+
query := fmt.Sprintf(`
318
+
UPDATE notifications
321
+
`, recipientFilter.Condition(), readFilter.Condition())
323
+
args := append(recipientFilter.Arg(), readFilter.Arg()...)
325
+
_, err := d.DB.ExecContext(ctx, query, args...)
327
+
return fmt.Errorf("failed to mark all notifications as read: %w", err)
333
+
func (d *DB) DeleteNotification(ctx context.Context, notificationID int64, userDID string) error {
334
+
idFilter := FilterEq("id", notificationID)
335
+
recipientFilter := FilterEq("recipient_did", userDID)
337
+
query := fmt.Sprintf(`
338
+
DELETE FROM notifications
340
+
`, idFilter.Condition(), recipientFilter.Condition())
342
+
args := append(idFilter.Arg(), recipientFilter.Arg()...)
344
+
result, err := d.DB.ExecContext(ctx, query, args...)
346
+
return fmt.Errorf("failed to delete notification: %w", err)
349
+
rowsAffected, err := result.RowsAffected()
351
+
return fmt.Errorf("failed to get rows affected: %w", err)
354
+
if rowsAffected == 0 {
355
+
return fmt.Errorf("notification not found or access denied")
361
+
func (d *DB) GetNotificationPreferences(ctx context.Context, userDID string) (*models.NotificationPreferences, error) {
362
+
userFilter := FilterEq("user_did", userDID)
364
+
query := fmt.Sprintf(`
365
+
SELECT id, user_did, repo_starred, issue_created, issue_commented, pull_created,
366
+
pull_commented, followed, pull_merged, issue_closed, email_notifications
367
+
FROM notification_preferences
369
+
`, userFilter.Condition())
371
+
var prefs models.NotificationPreferences
372
+
err := d.DB.QueryRowContext(ctx, query, userFilter.Arg()...).Scan(
375
+
&prefs.RepoStarred,
376
+
&prefs.IssueCreated,
377
+
&prefs.IssueCommented,
378
+
&prefs.PullCreated,
379
+
&prefs.PullCommented,
382
+
&prefs.IssueClosed,
383
+
&prefs.EmailNotifications,
387
+
if err == sql.ErrNoRows {
388
+
return &models.NotificationPreferences{
391
+
IssueCreated: true,
392
+
IssueCommented: true,
394
+
PullCommented: true,
398
+
EmailNotifications: false,
401
+
return nil, fmt.Errorf("failed to get notification preferences: %w", err)
407
+
func (d *DB) UpdateNotificationPreferences(ctx context.Context, prefs *models.NotificationPreferences) error {
409
+
INSERT OR REPLACE INTO notification_preferences
410
+
(user_did, repo_starred, issue_created, issue_commented, pull_created,
411
+
pull_commented, followed, pull_merged, issue_closed, email_notifications)
412
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
415
+
result, err := d.DB.ExecContext(ctx, query,
418
+
prefs.IssueCreated,
419
+
prefs.IssueCommented,
421
+
prefs.PullCommented,
425
+
prefs.EmailNotifications,
428
+
return fmt.Errorf("failed to update notification preferences: %w", err)
432
+
id, err := result.LastInsertId()
434
+
return fmt.Errorf("failed to get preferences ID: %w", err)
442
+
func (d *DB) ClearOldNotifications(ctx context.Context, olderThan time.Duration) error {
443
+
cutoff := time.Now().Add(-olderThan)
444
+
createdFilter := FilterLte("created", cutoff)
446
+
query := fmt.Sprintf(`
447
+
DELETE FROM notifications
449
+
`, createdFilter.Condition())
451
+
_, err := d.DB.ExecContext(ctx, query, createdFilter.Arg()...)
453
+
return fmt.Errorf("failed to cleanup old notifications: %w", err)