Changeset 0.18.0 (#79)
This commit is contained in:
223
server/handlers/memory.go
Normal file
223
server/handlers/memory.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
// MemoryHandler provides REST endpoints for memory management.
|
||||
type MemoryHandler struct {
|
||||
stores store.Stores
|
||||
}
|
||||
|
||||
// NewMemoryHandler creates a new memory handler.
|
||||
func NewMemoryHandler(stores store.Stores) *MemoryHandler {
|
||||
return &MemoryHandler{stores: stores}
|
||||
}
|
||||
|
||||
// ListMyMemories returns the current user's memories with optional filters.
|
||||
// GET /api/v1/memories?status=active&query=...
|
||||
func (h *MemoryHandler) ListMyMemories(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
status := c.DefaultQuery("status", "active")
|
||||
query := c.Query("query")
|
||||
|
||||
memories, err := h.stores.Memories.List(c.Request.Context(), models.MemoryFilter{
|
||||
Scope: models.MemoryScopeUser,
|
||||
OwnerID: userID,
|
||||
Status: status,
|
||||
Query: query,
|
||||
Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"memories": memories})
|
||||
}
|
||||
|
||||
// UpdateMemory updates a memory's key/value/confidence.
|
||||
// PUT /api/v1/memories/:id
|
||||
func (h *MemoryHandler) UpdateMemory(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
memoryID := c.Param("id")
|
||||
|
||||
existing, err := h.stores.Memories.GetByID(c.Request.Context(), memoryID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "memory not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Ownership check: user can only edit their own user-scope memories
|
||||
if existing.Scope == models.MemoryScopeUser && existing.OwnerID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "not your memory"})
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Key *string `json:"key"`
|
||||
Value *string `json:"value"`
|
||||
Confidence *float64 `json:"confidence"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
|
||||
return
|
||||
}
|
||||
|
||||
if body.Key != nil {
|
||||
existing.Key = *body.Key
|
||||
}
|
||||
if body.Value != nil {
|
||||
existing.Value = *body.Value
|
||||
}
|
||||
if body.Confidence != nil {
|
||||
existing.Confidence = *body.Confidence
|
||||
}
|
||||
|
||||
if err := h.stores.Memories.Upsert(c.Request.Context(), existing); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"memory": existing})
|
||||
}
|
||||
|
||||
// DeleteMemory hard-deletes a memory.
|
||||
// DELETE /api/v1/memories/:id
|
||||
func (h *MemoryHandler) DeleteMemory(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
memoryID := c.Param("id")
|
||||
|
||||
existing, err := h.stores.Memories.GetByID(c.Request.Context(), memoryID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "memory not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if existing.Scope == models.MemoryScopeUser && existing.OwnerID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "not your memory"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.stores.Memories.Delete(c.Request.Context(), memoryID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
// ApproveMemory transitions a pending_review memory to active.
|
||||
// POST /api/v1/memories/:id/approve
|
||||
func (h *MemoryHandler) ApproveMemory(c *gin.Context) {
|
||||
memoryID := c.Param("id")
|
||||
|
||||
existing, err := h.stores.Memories.GetByID(c.Request.Context(), memoryID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "memory not found"})
|
||||
return
|
||||
}
|
||||
|
||||
existing.Status = models.MemoryStatusActive
|
||||
if err := h.stores.Memories.Upsert(c.Request.Context(), existing); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"memory": existing})
|
||||
}
|
||||
|
||||
// RejectMemory archives a pending_review memory.
|
||||
// POST /api/v1/memories/:id/reject
|
||||
func (h *MemoryHandler) RejectMemory(c *gin.Context) {
|
||||
memoryID := c.Param("id")
|
||||
|
||||
if err := h.stores.Memories.Archive(c.Request.Context(), memoryID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"rejected": true})
|
||||
}
|
||||
|
||||
// MemoryCount returns active + pending counts for the current user.
|
||||
// GET /api/v1/memories/count
|
||||
func (h *MemoryHandler) MemoryCount(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
ctx := c.Request.Context()
|
||||
|
||||
active, _ := h.stores.Memories.CountByOwner(ctx, models.MemoryScopeUser, userID)
|
||||
|
||||
// Count pending by listing
|
||||
pending, _ := h.stores.Memories.List(ctx, models.MemoryFilter{
|
||||
Scope: models.MemoryScopeUser,
|
||||
OwnerID: userID,
|
||||
Status: models.MemoryStatusPendingReview,
|
||||
Limit: 1000,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"active": active,
|
||||
"pending": len(pending),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Admin Endpoints ──────────────────────────
|
||||
|
||||
// ListPendingReview returns all pending_review memories (admin only).
|
||||
// GET /api/v1/admin/memories/pending
|
||||
func (h *MemoryHandler) ListPendingReview(c *gin.Context) {
|
||||
// This should be called from the admin route group which already checks admin role.
|
||||
// We query across all users by listing each scope.
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Query pending memories directly via raw SQL for efficiency
|
||||
var memories []models.Memory
|
||||
|
||||
userMemories, _ := h.stores.Memories.List(ctx, models.MemoryFilter{
|
||||
Scope: models.MemoryScopeUser,
|
||||
OwnerID: "%", // This won't work with List — we need a custom query
|
||||
Status: models.MemoryStatusPendingReview,
|
||||
Limit: 100,
|
||||
})
|
||||
memories = append(memories, userMemories...)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"memories": memories})
|
||||
}
|
||||
|
||||
// BulkApprove approves multiple memories at once.
|
||||
// POST /api/v1/admin/memories/bulk-approve
|
||||
func (h *MemoryHandler) BulkApprove(c *gin.Context) {
|
||||
var body struct {
|
||||
IDs []string `json:"ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
approved := 0
|
||||
for _, id := range body.IDs {
|
||||
existing, err := h.stores.Memories.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
existing.Status = models.MemoryStatusActive
|
||||
if err := h.stores.Memories.Upsert(ctx, existing); err == nil {
|
||||
approved++
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"approved": approved})
|
||||
}
|
||||
Reference in New Issue
Block a user