Changeset 0.20.0 (#85)

This commit is contained in:
2026-03-01 12:40:15 +00:00
parent eb74180611
commit 817062e5fc
57 changed files with 5435 additions and 72 deletions

View File

@@ -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,

View 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}
}

View 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
}

View File

@@ -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(),
}
}

View File

@@ -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),