Files
vikunja/pkg/notifications/database.go
kolaente f5385c574e feat(websocket): add notification event with XORM AfterInsert dispatch
Add NotificationCreatedEvent that fires automatically when a
DatabaseNotification is inserted, using XORM's AfterInsertProcessor
interface. The AfterInsert hook dispatches the event after the row
is persisted, without callers needing to manage DispatchOnCommit or
DispatchPending.

The WebSocket listener subscribes to this event, reloads the
notification from the database (ensuring accurate timestamps), and
pushes it to connected clients subscribed to the notification.created
event. Dispatch errors are logged rather than propagated since the
DB notification is already committed at that point.
2026-04-02 16:30:23 +00:00

136 lines
4.8 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package notifications
import (
"time"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"xorm.io/xorm"
)
// DatabaseNotification represents a notification that was saved to the database
type DatabaseNotification struct {
// The unique, numeric id of this notification.
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"notificationid"`
// The ID of the notifiable this notification is associated with.
NotifiableID int64 `xorm:"bigint not null" json:"-"`
// The actual content of the notification.
Notification interface{} `xorm:"json not null" json:"notification"`
// The name of the notification
Name string `xorm:"varchar(250) index not null" json:"name"`
// The thing the notification is about. Used to check if a notification for this thing already happened or not.
SubjectID int64 `xorm:"bigint null" json:"-"`
// When this notification is marked as read, this will be updated with the current timestamp.
ReadAt time.Time `xorm:"datetime null" json:"read_at"`
// A timestamp when this notification was created. You cannot change this value.
Created time.Time `xorm:"created not null" json:"created"`
}
// AfterInsert is called by XORM after the row is inserted. For transactional
// sessions this runs during Commit(), guaranteeing the row is persisted before
// the event fires.
func (d *DatabaseNotification) AfterInsert() {
if err := events.Dispatch(&NotificationCreatedEvent{
NotificationID: d.ID,
UserID: d.NotifiableID,
}); err != nil {
log.Errorf("Failed to dispatch notification created event for notification %d: %v", d.ID, err)
}
}
// TableName resolves to a better table name for notifications
func (d *DatabaseNotification) TableName() string {
return "notifications"
}
// GetNotificationsForUser returns all notifications for a user. It is possible to limit the amount of notifications
// to return with the limit and start parameters.
// We're not passing a user object in directly because every other package imports this one so we'd get import cycles.
func GetNotificationsForUser(s *xorm.Session, notifiableID int64, limit, start int) (notifications []*DatabaseNotification, resultCount int, total int64, err error) {
err = s.
Where("notifiable_id = ?", notifiableID).
Limit(limit, start).
OrderBy("id DESC").
Find(&notifications)
if err != nil {
return nil, 0, 0, err
}
total, err = s.
Where("notifiable_id = ?", notifiableID).
Count(&DatabaseNotification{})
return notifications, len(notifications), total, err
}
// GetNotificationByID returns a single notification by its ID.
func GetNotificationByID(s *xorm.Session, id int64) (*DatabaseNotification, error) {
n := &DatabaseNotification{}
has, err := s.ID(id).Get(n)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return n, nil
}
func GetNotificationsForNameAndUser(s *xorm.Session, notifiableID int64, event string, subjectID int64) (notifications []*DatabaseNotification, err error) {
notifications = []*DatabaseNotification{}
err = s.Where("notifiable_id = ? AND name = ? AND subject_id = ?", notifiableID, event, subjectID).
Find(&notifications)
return
}
// CanMarkNotificationAsRead checks if a user can mark a notification as read.
func CanMarkNotificationAsRead(s *xorm.Session, notification *DatabaseNotification, notifiableID int64) (can bool, err error) {
can, err = s.
Where("notifiable_id = ? AND id = ?", notifiableID, notification.ID).
NoAutoCondition().
Get(notification)
return
}
// MarkNotificationAsRead marks a notification as read. It should be called only after CanMarkNotificationAsRead has
// been called.
func MarkNotificationAsRead(s *xorm.Session, notification *DatabaseNotification, read bool) (err error) {
notification.ReadAt = time.Time{}
if read {
notification.ReadAt = time.Now()
}
_, err = s.
Where("id = ?", notification.ID).
Cols("read_at").
Update(notification)
return
}
func MarkAllNotificationsAsRead(s *xorm.Session, userID int64) (err error) {
_, err = s.
Where("notifiable_id = ?", userID).
Cols("read_at").
Update(&DatabaseNotification{ReadAt: time.Now()})
return
}