Changeset 0.12.0 (#63)

This commit is contained in:
2026-02-25 21:38:49 +00:00
parent c9d8e9457e
commit 88216ec4cb
59 changed files with 13115 additions and 139 deletions

View File

@@ -2,6 +2,7 @@ package handlers
import (
"database/sql"
"encoding/json"
"math"
"net/http"
"strconv"
@@ -27,33 +28,35 @@ type createChannelRequest struct {
}
type updateChannelRequest struct {
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
Model *string `json:"model,omitempty"`
SystemPrompt *string `json:"system_prompt,omitempty"`
APIConfigID *string `json:"provider_config_id,omitempty"`
IsArchived *bool `json:"is_archived,omitempty"`
IsPinned *bool `json:"is_pinned,omitempty"`
Folder *string `json:"folder,omitempty"`
Tags []string `json:"tags,omitempty"`
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
Model *string `json:"model,omitempty"`
SystemPrompt *string `json:"system_prompt,omitempty"`
APIConfigID *string `json:"provider_config_id,omitempty"`
IsArchived *bool `json:"is_archived,omitempty"`
IsPinned *bool `json:"is_pinned,omitempty"`
Folder *string `json:"folder,omitempty"`
Tags []string `json:"tags,omitempty"`
Settings *json.RawMessage `json:"settings,omitempty"` // JSONB merge into existing settings
}
type channelResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Title string `json:"title"`
Type string `json:"type"`
Description *string `json:"description"`
Model *string `json:"model"`
APIConfigID *string `json:"provider_config_id"`
SystemPrompt *string `json:"system_prompt"`
IsArchived bool `json:"is_archived"`
IsPinned bool `json:"is_pinned"`
Folder *string `json:"folder"`
Tags []string `json:"tags"`
MessageCount int `json:"message_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ID string `json:"id"`
UserID string `json:"user_id"`
Title string `json:"title"`
Type string `json:"type"`
Description *string `json:"description"`
Model *string `json:"model"`
APIConfigID *string `json:"provider_config_id"`
SystemPrompt *string `json:"system_prompt"`
IsArchived bool `json:"is_archived"`
IsPinned bool `json:"is_pinned"`
Folder *string `json:"folder"`
Tags []string `json:"tags"`
Settings json.RawMessage `json:"settings,omitempty"`
MessageCount int `json:"message_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type paginatedResponse struct {
@@ -72,6 +75,16 @@ func NewChannelHandler() *ChannelHandler {
return &ChannelHandler{}
}
// channelDeleteHook is called after a channel is successfully deleted.
// Set at startup by SetChannelDeleteHook to clean up storage files.
var channelDeleteHook func(channelID string)
// SetChannelDeleteHook registers a callback invoked after channel deletion.
// Used to clean up attachment files on the storage backend.
func SetChannelDeleteHook(fn func(channelID string)) {
channelDeleteHook = fn
}
// ── Helpers ─────────────────────────────────
// getUserID extracts the authenticated user's ID from context.
@@ -141,7 +154,7 @@ func (h *ChannelHandler) ListChannels(c *gin.Context) {
// Fetch channels with message count
query := `
SELECT c.id, c.user_id, c.title, c.type, c.description, c.model, c.provider_config_id,
c.system_prompt, c.is_archived, c.is_pinned, c.folder, c.tags,
c.system_prompt, c.is_archived, c.is_pinned, c.folder, c.tags, c.settings,
COALESCE(mc.cnt, 0) AS message_count,
c.created_at, c.updated_at
FROM channels c
@@ -186,7 +199,7 @@ func (h *ChannelHandler) ListChannels(c *gin.Context) {
err := rows.Scan(
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.APIConfigID,
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder,
pq.Array(&tags),
pq.Array(&tags), &ch.Settings,
&ch.MessageCount, &ch.CreatedAt, &ch.UpdatedAt,
)
if err != nil {
@@ -236,13 +249,13 @@ func (h *ChannelHandler) CreateChannel(c *gin.Context) {
INSERT INTO channels (user_id, title, type, description, model, system_prompt, provider_config_id, folder, tags)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id, user_id, title, type, description, model, provider_config_id, system_prompt,
is_archived, is_pinned, folder, tags, created_at, updated_at
is_archived, is_pinned, folder, tags, settings, created_at, updated_at
`, userID, req.Title, channelType, req.Description, req.Model, req.SystemPrompt, req.APIConfigID,
req.Folder, pq.Array(req.Tags),
).Scan(
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.APIConfigID,
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder,
pq.Array(&tags), &ch.CreatedAt, &ch.UpdatedAt,
pq.Array(&tags), &ch.Settings, &ch.CreatedAt, &ch.UpdatedAt,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create channel"})
@@ -284,7 +297,7 @@ func (h *ChannelHandler) GetChannel(c *gin.Context) {
var tags []string
err := database.DB.QueryRow(`
SELECT c.id, c.user_id, c.title, c.type, c.description, c.model, c.provider_config_id,
c.system_prompt, c.is_archived, c.is_pinned, c.folder, c.tags,
c.system_prompt, c.is_archived, c.is_pinned, c.folder, c.tags, c.settings,
COALESCE(mc.cnt, 0) AS message_count,
c.created_at, c.updated_at
FROM channels c
@@ -295,7 +308,7 @@ func (h *ChannelHandler) GetChannel(c *gin.Context) {
`, channelID, userID).Scan(
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.APIConfigID,
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder,
pq.Array(&tags),
pq.Array(&tags), &ch.Settings,
&ch.MessageCount, &ch.CreatedAt, &ch.UpdatedAt,
)
@@ -383,6 +396,12 @@ func (h *ChannelHandler) UpdateChannel(c *gin.Context) {
if req.Tags != nil {
addClause("tags", pq.Array(req.Tags))
}
if req.Settings != nil {
// JSONB merge: new settings keys overwrite existing, unmentioned keys preserved
setClauses = append(setClauses, "settings = COALESCE(settings, '{}'::jsonb) || $"+strconv.Itoa(argN)+"::jsonb")
args = append(args, []byte(*req.Settings))
argN++
}
if len(setClauses) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
@@ -430,5 +449,10 @@ func (h *ChannelHandler) DeleteChannel(c *gin.Context) {
return
}
// Clean up storage files (CASCADE already removed PG attachment rows)
if channelDeleteHook != nil {
go channelDeleteHook(channelID)
}
c.JSON(http.StatusOK, gin.H{"message": "channel deleted"})
}