This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/folders.go
2026-03-06 13:26:25 +00:00

147 lines
4.0 KiB
Go

package handlers
// folders.go — Chat folder CRUD (v0.23.1)
//
// Folders are user-scoped groupings for personal (direct) chats.
// Routes:
// GET /api/v1/folders
// POST /api/v1/folders
// PUT /api/v1/folders/:id
// DELETE /api/v1/folders/:id
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"git.gobha.me/xcaliber/chat-switchboard/database"
)
type FolderHandler struct{}
func NewFolderHandler() *FolderHandler { return &FolderHandler{} }
type folderRow struct {
ID string `json:"id"`
Name string `json:"name"`
ParentID *string `json:"parent_id,omitempty"`
SortOrder int `json:"sort_order"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (h *FolderHandler) List(c *gin.Context) {
userID := getUserID(c)
rows, err := database.DB.QueryContext(c.Request.Context(), database.Q(`
SELECT id, name, parent_id, sort_order, created_at, updated_at
FROM folders
WHERE user_id = $1
ORDER BY sort_order, name
`), userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list folders"})
return
}
defer rows.Close()
var folders []folderRow
for rows.Next() {
var f folderRow
if err := rows.Scan(&f.ID, &f.Name, &f.ParentID, &f.SortOrder, &f.CreatedAt, &f.UpdatedAt); err != nil {
continue
}
folders = append(folders, f)
}
if folders == nil {
folders = []folderRow{}
}
c.JSON(http.StatusOK, gin.H{"folders": folders})
}
func (h *FolderHandler) Create(c *gin.Context) {
userID := getUserID(c)
var req struct {
Name string `json:"name" binding:"required"`
SortOrder int `json:"sort_order"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
req.Name = strings.TrimSpace(req.Name)
if req.Name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
return
}
var f folderRow
err := database.DB.QueryRowContext(c.Request.Context(), database.Q(`
INSERT INTO folders (user_id, name, sort_order)
VALUES ($1, $2, $3)
RETURNING id, name, parent_id, sort_order, created_at, updated_at
`), userID, req.Name, req.SortOrder).Scan(
&f.ID, &f.Name, &f.ParentID, &f.SortOrder, &f.CreatedAt, &f.UpdatedAt)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create folder"})
return
}
c.JSON(http.StatusCreated, gin.H{"folder": f})
}
func (h *FolderHandler) Update(c *gin.Context) {
userID := getUserID(c)
folderID := c.Param("id")
var req struct {
Name string `json:"name"`
SortOrder *int `json:"sort_order"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Name != "" {
req.Name = strings.TrimSpace(req.Name)
}
res, err := database.DB.ExecContext(c.Request.Context(), database.Q(`
UPDATE folders
SET name = COALESCE(NULLIF($3, ''), name),
sort_order = COALESCE($4, sort_order)
WHERE id = $1 AND user_id = $2
`), folderID, userID, req.Name, req.SortOrder)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update folder"})
return
}
if n, _ := res.RowsAffected(); n == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "folder not found"})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (h *FolderHandler) Delete(c *gin.Context) {
userID := getUserID(c)
folderID := c.Param("id")
// Unassign chats before deleting folder
_, _ = database.DB.ExecContext(c.Request.Context(), database.Q(`
UPDATE channels SET folder_id = NULL WHERE folder_id = $1 AND user_id = $2
`), folderID, userID)
res, err := database.DB.ExecContext(c.Request.Context(), database.Q(`
DELETE FROM folders WHERE id = $1 AND user_id = $2
`), folderID, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete folder"})
return
}
if n, _ := res.RowsAffected(); n == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "folder not found"})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}