package sqlite import ( "context" "database/sql" "time" "switchboard-core/models" "switchboard-core/store" ) type NotificationStore struct{} func NewNotificationStore() *NotificationStore { return &NotificationStore{} } func (s *NotificationStore) Create(ctx context.Context, n *models.Notification) error { if n.ID == "" { n.ID = store.NewID() } now := time.Now().UTC().Format(timeFmt) _, err := DB.ExecContext(ctx, ` INSERT INTO notifications (id, user_id, type, title, body, resource_type, resource_id, is_read, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, n.ID, n.UserID, n.Type, n.Title, n.Body, nullStr(n.ResourceType), nullStr(n.ResourceID), boolToInt(n.IsRead), now, ) if err != nil { return err } n.CreatedAt, _ = time.Parse(timeFmt, now) return nil } func (s *NotificationStore) ListByUser(ctx context.Context, userID string, limit, offset int, unreadOnly bool) ([]models.Notification, int, error) { if limit <= 0 { limit = 20 } // Count countQ := `SELECT COUNT(*) FROM notifications WHERE user_id = ?` countArgs := []interface{}{userID} if unreadOnly { countQ += ` AND is_read = 0` } var total int if err := DB.QueryRowContext(ctx, countQ, countArgs...).Scan(&total); err != nil { return nil, 0, err } // Fetch q := `SELECT id, user_id, type, title, body, resource_type, resource_id, is_read, created_at FROM notifications WHERE user_id = ?` if unreadOnly { q += ` AND is_read = 0` } q += ` ORDER BY created_at DESC LIMIT ? OFFSET ?` rows, err := DB.QueryContext(ctx, q, userID, limit, offset) if err != nil { return nil, 0, err } defer rows.Close() var items []models.Notification for rows.Next() { var n models.Notification var resType, resID sql.NullString var isRead int if err := rows.Scan( &n.ID, &n.UserID, &n.Type, &n.Title, &n.Body, &resType, &resID, &isRead, st(&n.CreatedAt), ); err != nil { return nil, 0, err } n.ResourceType = NullableString(resType) n.ResourceID = NullableString(resID) n.IsRead = isRead != 0 items = append(items, n) } if items == nil { items = []models.Notification{} } return items, total, rows.Err() } func (s *NotificationStore) MarkRead(ctx context.Context, id, userID string) error { res, err := DB.ExecContext(ctx, `UPDATE notifications SET is_read = 1 WHERE id = ? AND user_id = ?`, id, userID) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return sql.ErrNoRows } return nil } func (s *NotificationStore) MarkAllRead(ctx context.Context, userID string) error { _, err := DB.ExecContext(ctx, `UPDATE notifications SET is_read = 1 WHERE user_id = ? AND is_read = 0`, userID) return err } func (s *NotificationStore) Delete(ctx context.Context, id, userID string) error { res, err := DB.ExecContext(ctx, `DELETE FROM notifications WHERE id = ? AND user_id = ?`, id, userID) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return sql.ErrNoRows } return nil } func (s *NotificationStore) UnreadCount(ctx context.Context, userID string) (int, error) { var count int err := DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM notifications WHERE user_id = ? AND is_read = 0`, userID).Scan(&count) return count, err } func (s *NotificationStore) DeleteOlderThan(ctx context.Context, before time.Time) (int64, error) { res, err := DB.ExecContext(ctx, `DELETE FROM notifications WHERE created_at < ?`, before.UTC().Format(timeFmt)) if err != nil { return 0, err } return res.RowsAffected() } // nullStr returns sql.NullString for optional string fields. func nullStr(s string) sql.NullString { if s == "" { return sql.NullString{} } return sql.NullString{String: s, Valid: true} }