Changeset 0.20.0 (#85)
This commit is contained in:
@@ -40,6 +40,8 @@ type Stores struct {
|
||||
ResourceGrants ResourceGrantStore
|
||||
Memories MemoryStore
|
||||
Projects ProjectStore
|
||||
Notifications NotificationStore
|
||||
NotifPrefs NotificationPreferenceStore
|
||||
}
|
||||
|
||||
// =========================================
|
||||
@@ -216,6 +218,9 @@ type ChannelStore interface {
|
||||
// Channel models
|
||||
SetModel(ctx context.Context, cm *models.ChannelModel) error
|
||||
GetModels(ctx context.Context, channelID string) ([]models.ChannelModel, error)
|
||||
GetModelByID(ctx context.Context, id string) (*models.ChannelModel, error)
|
||||
UpdateModel(ctx context.Context, id string, fields map[string]interface{}) error
|
||||
DeleteModel(ctx context.Context, id string) error
|
||||
|
||||
// Ownership check
|
||||
UserOwns(ctx context.Context, channelID, userID string) (bool, error)
|
||||
@@ -476,6 +481,35 @@ type ResourceGrantStore interface {
|
||||
UserHasGroupAccess(ctx context.Context, userID, resourceType, resourceID string) (bool, error)
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// NOTIFICATION STORE (v0.20.0)
|
||||
// =========================================
|
||||
|
||||
type NotificationStore interface {
|
||||
Create(ctx context.Context, n *models.Notification) error
|
||||
ListByUser(ctx context.Context, userID string, limit, offset int, unreadOnly bool) ([]models.Notification, int, error)
|
||||
MarkRead(ctx context.Context, id, userID string) error
|
||||
MarkAllRead(ctx context.Context, userID string) error
|
||||
Delete(ctx context.Context, id, userID string) error
|
||||
UnreadCount(ctx context.Context, userID string) (int, error)
|
||||
DeleteOlderThan(ctx context.Context, before time.Time) (int64, error)
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// NOTIFICATION PREFERENCES STORE (v0.20.0)
|
||||
// =========================================
|
||||
|
||||
type NotificationPreferenceStore interface {
|
||||
// Get returns the preference for a specific user + type. Returns nil if not set.
|
||||
Get(ctx context.Context, userID, notifType string) (*models.NotificationPreference, error)
|
||||
// ListForUser returns all preferences set by a user.
|
||||
ListForUser(ctx context.Context, userID string) ([]models.NotificationPreference, error)
|
||||
// Upsert creates or updates a preference row.
|
||||
Upsert(ctx context.Context, pref *models.NotificationPreference) error
|
||||
// Delete removes a preference (falls back to default).
|
||||
Delete(ctx context.Context, userID, notifType string) error
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// SHARED TYPES
|
||||
// =========================================
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
@@ -212,6 +214,51 @@ func (s *ChannelStore) GetModels(ctx context.Context, channelID string) ([]model
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (s *ChannelStore) GetModelByID(ctx context.Context, id string) (*models.ChannelModel, error) {
|
||||
var cm models.ChannelModel
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, channel_id, model_id, COALESCE(provider_config_id::text, ''),
|
||||
COALESCE(display_name, ''), COALESCE(system_prompt, ''), is_default
|
||||
FROM channel_models WHERE id = $1`, id).Scan(
|
||||
&cm.ID, &cm.ChannelID, &cm.ModelID, &cm.ProviderConfigID,
|
||||
&cm.DisplayName, &cm.SystemPrompt, &cm.IsDefault)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cm, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) UpdateModel(ctx context.Context, id string, fields map[string]interface{}) error {
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Build SET clause dynamically
|
||||
sets := make([]string, 0, len(fields))
|
||||
args := make([]interface{}, 0, len(fields)+1)
|
||||
i := 1
|
||||
for col, val := range fields {
|
||||
sets = append(sets, col+" = $"+fmt.Sprintf("%d", i))
|
||||
args = append(args, val)
|
||||
i++
|
||||
}
|
||||
args = append(args, id)
|
||||
query := "UPDATE channel_models SET " + strings.Join(sets, ", ") + " WHERE id = $" + fmt.Sprintf("%d", i)
|
||||
_, err := DB.ExecContext(ctx, query, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ChannelStore) DeleteModel(ctx context.Context, id string) error {
|
||||
res, err := DB.ExecContext(ctx, `DELETE FROM channel_models WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) UserOwns(ctx context.Context, channelID, userID string) (bool, error) {
|
||||
var exists bool
|
||||
err := DB.QueryRowContext(ctx,
|
||||
|
||||
133
server/store/postgres/notification.go
Normal file
133
server/store/postgres/notification.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
)
|
||||
|
||||
type NotificationStore struct{}
|
||||
|
||||
func NewNotificationStore() *NotificationStore { return &NotificationStore{} }
|
||||
|
||||
func (s *NotificationStore) Create(ctx context.Context, n *models.Notification) error {
|
||||
return DB.QueryRowContext(ctx, `
|
||||
INSERT INTO notifications (user_id, type, title, body, resource_type, resource_id, is_read)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, created_at`,
|
||||
n.UserID, n.Type, n.Title, n.Body,
|
||||
nullStr(n.ResourceType), nullStr(n.ResourceID), n.IsRead,
|
||||
).Scan(&n.ID, &n.CreatedAt)
|
||||
}
|
||||
|
||||
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 = $1`
|
||||
args := []interface{}{userID}
|
||||
if unreadOnly {
|
||||
countQ += ` AND is_read = false`
|
||||
}
|
||||
|
||||
var total int
|
||||
if err := DB.QueryRowContext(ctx, countQ, args...).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 = $1`
|
||||
if unreadOnly {
|
||||
q += ` AND is_read = false`
|
||||
}
|
||||
q += ` ORDER BY created_at DESC LIMIT $2 OFFSET $3`
|
||||
|
||||
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
|
||||
if err := rows.Scan(
|
||||
&n.ID, &n.UserID, &n.Type, &n.Title, &n.Body,
|
||||
&resType, &resID, &n.IsRead, &n.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
n.ResourceType = NullableString(resType)
|
||||
n.ResourceID = NullableString(resID)
|
||||
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 = true WHERE id = $1 AND user_id = $2`,
|
||||
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 = true WHERE user_id = $1 AND is_read = false`,
|
||||
userID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *NotificationStore) Delete(ctx context.Context, id, userID string) error {
|
||||
res, err := DB.ExecContext(ctx,
|
||||
`DELETE FROM notifications WHERE id = $1 AND user_id = $2`, 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 = $1 AND is_read = false`,
|
||||
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 < $1`, before)
|
||||
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}
|
||||
}
|
||||
69
server/store/postgres/notification_prefs.go
Normal file
69
server/store/postgres/notification_prefs.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
)
|
||||
|
||||
type NotificationPreferenceStore struct{}
|
||||
|
||||
func NewNotificationPreferenceStore() *NotificationPreferenceStore {
|
||||
return &NotificationPreferenceStore{}
|
||||
}
|
||||
|
||||
func (s *NotificationPreferenceStore) Get(ctx context.Context, userID, notifType string) (*models.NotificationPreference, error) {
|
||||
var p models.NotificationPreference
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, user_id, type, in_app, email
|
||||
FROM notification_preferences
|
||||
WHERE user_id = $1 AND type = $2`, userID, notifType).Scan(
|
||||
&p.ID, &p.UserID, &p.Type, &p.InApp, &p.Email)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (s *NotificationPreferenceStore) ListForUser(ctx context.Context, userID string) ([]models.NotificationPreference, error) {
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT id, user_id, type, in_app, email
|
||||
FROM notification_preferences
|
||||
WHERE user_id = $1
|
||||
ORDER BY type`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []models.NotificationPreference
|
||||
for rows.Next() {
|
||||
var p models.NotificationPreference
|
||||
if err := rows.Scan(&p.ID, &p.UserID, &p.Type, &p.InApp, &p.Email); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, p)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (s *NotificationPreferenceStore) Upsert(ctx context.Context, pref *models.NotificationPreference) error {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO notification_preferences (user_id, type, in_app, email)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (user_id, type) DO UPDATE SET
|
||||
in_app = $3, email = $4`,
|
||||
pref.UserID, pref.Type, pref.InApp, pref.Email)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *NotificationPreferenceStore) Delete(ctx context.Context, userID, notifType string) error {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
DELETE FROM notification_preferences
|
||||
WHERE user_id = $1 AND type = $2`, userID, notifType)
|
||||
return err
|
||||
}
|
||||
@@ -11,27 +11,29 @@ import (
|
||||
func NewStores(db *sql.DB) store.Stores {
|
||||
SetDB(db)
|
||||
return store.Stores{
|
||||
Providers: NewProviderStore(),
|
||||
Catalog: NewCatalogStore(),
|
||||
Personas: NewPersonaStore(),
|
||||
Policies: NewPolicyStore(),
|
||||
UserSettings: NewUserModelSettingsStore(),
|
||||
Users: NewUserStore(),
|
||||
Teams: NewTeamStore(),
|
||||
Channels: NewChannelStore(),
|
||||
Messages: NewMessageStore(),
|
||||
Audit: NewAuditStore(),
|
||||
Notes: NewNoteStore(),
|
||||
NoteLinks: NewNoteLinkStore(),
|
||||
GlobalConfig: NewGlobalConfigStore(),
|
||||
Usage: NewUsageStore(),
|
||||
Pricing: NewPricingStore(),
|
||||
Extensions: NewExtensionStore(),
|
||||
Attachments: NewAttachmentStore(),
|
||||
Providers: NewProviderStore(),
|
||||
Catalog: NewCatalogStore(),
|
||||
Personas: NewPersonaStore(),
|
||||
Policies: NewPolicyStore(),
|
||||
UserSettings: NewUserModelSettingsStore(),
|
||||
Users: NewUserStore(),
|
||||
Teams: NewTeamStore(),
|
||||
Channels: NewChannelStore(),
|
||||
Messages: NewMessageStore(),
|
||||
Audit: NewAuditStore(),
|
||||
Notes: NewNoteStore(),
|
||||
NoteLinks: NewNoteLinkStore(),
|
||||
GlobalConfig: NewGlobalConfigStore(),
|
||||
Usage: NewUsageStore(),
|
||||
Pricing: NewPricingStore(),
|
||||
Extensions: NewExtensionStore(),
|
||||
Attachments: NewAttachmentStore(),
|
||||
KnowledgeBases: NewKnowledgeBaseStore(),
|
||||
Groups: NewGroupStore(),
|
||||
ResourceGrants: NewResourceGrantStore(),
|
||||
Memories: NewMemoryStore(),
|
||||
Projects: NewProjectStore(),
|
||||
Notifications: NewNotificationStore(),
|
||||
NotifPrefs: NewNotificationPreferenceStore(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ func NewUserModelSettingsStore() *UserModelSettingsStore { return &UserModelSett
|
||||
|
||||
func (s *UserModelSettingsStore) GetForUser(ctx context.Context, userID string) ([]models.UserModelSetting, error) {
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT id, user_id, model_id, hidden, preferred_temperature, preferred_max_tokens,
|
||||
sort_order, created_at, updated_at
|
||||
SELECT id, user_id, model_id, COALESCE(hidden, false), preferred_temperature, preferred_max_tokens,
|
||||
COALESCE(sort_order, 0), created_at, updated_at
|
||||
FROM user_model_settings WHERE user_id = $1 ORDER BY sort_order, model_id`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -91,7 +91,7 @@ func (s *UserModelSettingsStore) Set(ctx context.Context, userID, modelID string
|
||||
// Build a custom upsert since the update builder doesn't handle INSERT ON CONFLICT
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO user_model_settings (user_id, model_id, hidden, preferred_temperature, preferred_max_tokens, sort_order)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
VALUES ($1, $2, COALESCE($3, false), $4, $5, COALESCE($6, 0))
|
||||
ON CONFLICT (user_id, model_id)
|
||||
DO UPDATE SET
|
||||
hidden = COALESCE($3, user_model_settings.hidden),
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
@@ -218,6 +219,48 @@ func (s *ChannelStore) GetModels(ctx context.Context, channelID string) ([]model
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (s *ChannelStore) GetModelByID(ctx context.Context, id string) (*models.ChannelModel, error) {
|
||||
var cm models.ChannelModel
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, channel_id, model_id, COALESCE(provider_config_id, ''),
|
||||
COALESCE(display_name, ''), COALESCE(system_prompt, ''), is_default
|
||||
FROM channel_models WHERE id = ?`, id).Scan(
|
||||
&cm.ID, &cm.ChannelID, &cm.ModelID, &cm.ProviderConfigID,
|
||||
&cm.DisplayName, &cm.SystemPrompt, &cm.IsDefault)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cm, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) UpdateModel(ctx context.Context, id string, fields map[string]interface{}) error {
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
sets := make([]string, 0, len(fields))
|
||||
args := make([]interface{}, 0, len(fields)+1)
|
||||
for col, val := range fields {
|
||||
sets = append(sets, col+" = ?")
|
||||
args = append(args, val)
|
||||
}
|
||||
args = append(args, id)
|
||||
query := "UPDATE channel_models SET " + strings.Join(sets, ", ") + " WHERE id = ?"
|
||||
_, err := DB.ExecContext(ctx, query, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ChannelStore) DeleteModel(ctx context.Context, id string) error {
|
||||
res, err := DB.ExecContext(ctx, `DELETE FROM channel_models WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) UserOwns(ctx context.Context, channelID, userID string) (bool, error) {
|
||||
var exists bool
|
||||
err := DB.QueryRowContext(ctx,
|
||||
|
||||
146
server/store/sqlite/notification.go
Normal file
146
server/store/sqlite/notification.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/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}
|
||||
}
|
||||
70
server/store/sqlite/notification_prefs.go
Normal file
70
server/store/sqlite/notification_prefs.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
type NotificationPreferenceStore struct{}
|
||||
|
||||
func NewNotificationPreferenceStore() *NotificationPreferenceStore {
|
||||
return &NotificationPreferenceStore{}
|
||||
}
|
||||
|
||||
func (s *NotificationPreferenceStore) Get(ctx context.Context, userID, notifType string) (*models.NotificationPreference, error) {
|
||||
var p models.NotificationPreference
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, user_id, type, in_app, email
|
||||
FROM notification_preferences
|
||||
WHERE user_id = ? AND type = ?`, userID, notifType).Scan(
|
||||
&p.ID, &p.UserID, &p.Type, &p.InApp, &p.Email)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (s *NotificationPreferenceStore) ListForUser(ctx context.Context, userID string) ([]models.NotificationPreference, error) {
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT id, user_id, type, in_app, email
|
||||
FROM notification_preferences
|
||||
WHERE user_id = ?
|
||||
ORDER BY type`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []models.NotificationPreference
|
||||
for rows.Next() {
|
||||
var p models.NotificationPreference
|
||||
if err := rows.Scan(&p.ID, &p.UserID, &p.Type, &p.InApp, &p.Email); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, p)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (s *NotificationPreferenceStore) Upsert(ctx context.Context, pref *models.NotificationPreference) error {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO notification_preferences (id, user_id, type, in_app, email)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT (user_id, type) DO UPDATE SET
|
||||
in_app = excluded.in_app, email = excluded.email`,
|
||||
store.NewID(), pref.UserID, pref.Type, pref.InApp, pref.Email)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *NotificationPreferenceStore) Delete(ctx context.Context, userID, notifType string) error {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
DELETE FROM notification_preferences
|
||||
WHERE user_id = ? AND type = ?`, userID, notifType)
|
||||
return err
|
||||
}
|
||||
@@ -33,5 +33,7 @@ func NewStores(db *sql.DB) store.Stores {
|
||||
ResourceGrants: NewResourceGrantStore(),
|
||||
Memories: NewMemoryStore(),
|
||||
Projects: NewProjectStore(),
|
||||
Notifications: NewNotificationStore(),
|
||||
NotifPrefs: NewNotificationPreferenceStore(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ func NewUserModelSettingsStore() *UserModelSettingsStore { return &UserModelSett
|
||||
|
||||
func (s *UserModelSettingsStore) GetForUser(ctx context.Context, userID string) ([]models.UserModelSetting, error) {
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT id, user_id, model_id, hidden, preferred_temperature, preferred_max_tokens,
|
||||
sort_order, created_at, updated_at
|
||||
SELECT id, user_id, model_id, COALESCE(hidden, 0), preferred_temperature, preferred_max_tokens,
|
||||
COALESCE(sort_order, 0), created_at, updated_at
|
||||
FROM user_model_settings WHERE user_id = ? ORDER BY sort_order, model_id`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -92,7 +92,7 @@ func (s *UserModelSettingsStore) Set(ctx context.Context, userID, modelID string
|
||||
// Build a custom upsert since the update builder doesn't handle INSERT ON CONFLICT
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO user_model_settings (id, user_id, model_id, hidden, preferred_temperature, preferred_max_tokens, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, COALESCE(?, 0), ?, ?, COALESCE(?, 0))
|
||||
ON CONFLICT (user_id, model_id)
|
||||
DO UPDATE SET
|
||||
hidden = COALESCE(excluded.hidden, user_model_settings.hidden),
|
||||
|
||||
Reference in New Issue
Block a user