Changeset 0.17.3 (#78)
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/notelinks"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
@@ -20,6 +21,7 @@ type createNoteRequest struct {
|
||||
FolderPath string `json:"folder_path"`
|
||||
Tags []string `json:"tags"`
|
||||
SourceChannelID string `json:"source_channel_id"`
|
||||
SourceMessageID string `json:"source_message_id"`
|
||||
}
|
||||
|
||||
type updateNoteRequest struct {
|
||||
@@ -38,6 +40,7 @@ type noteResponse struct {
|
||||
FolderPath string `json:"folder_path"`
|
||||
Tags []string `json:"tags"`
|
||||
SourceChannelID *string `json:"source_channel_id,omitempty"`
|
||||
SourceMessageID *string `json:"source_message_id,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
@@ -85,6 +88,7 @@ func toNoteResponse(n *models.Note) noteResponse {
|
||||
FolderPath: n.FolderPath,
|
||||
Tags: tags,
|
||||
SourceChannelID: n.SourceChannelID,
|
||||
SourceMessageID: n.SourceMessageID,
|
||||
CreatedAt: n.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
UpdatedAt: n.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
@@ -132,6 +136,10 @@ func (h *NoteHandler) Create(c *gin.Context) {
|
||||
if req.SourceChannelID != "" {
|
||||
sourceChannelID = &req.SourceChannelID
|
||||
}
|
||||
var sourceMessageID *string
|
||||
if req.SourceMessageID != "" {
|
||||
sourceMessageID = &req.SourceMessageID
|
||||
}
|
||||
|
||||
note := &models.Note{
|
||||
UserID: userID,
|
||||
@@ -140,6 +148,7 @@ func (h *NoteHandler) Create(c *gin.Context) {
|
||||
FolderPath: folder,
|
||||
Tags: tags,
|
||||
SourceChannelID: sourceChannelID,
|
||||
SourceMessageID: sourceMessageID,
|
||||
}
|
||||
|
||||
if err := h.stores.Notes.Create(c.Request.Context(), note); err != nil {
|
||||
@@ -147,6 +156,14 @@ func (h *NoteHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract and store wikilinks
|
||||
h.extractAndStoreLinks(c, note.ID, note.Content, userID)
|
||||
|
||||
// Resolve dangling links from other notes that reference this title
|
||||
if h.stores.NoteLinks != nil {
|
||||
h.stores.NoteLinks.ResolveByTitle(c.Request.Context(), userID, note.ID, note.Title)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, toNoteResponse(note))
|
||||
}
|
||||
|
||||
@@ -235,6 +252,11 @@ func (h *NoteHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Re-extract links if content changed
|
||||
if req.Content != nil {
|
||||
h.extractAndStoreLinks(c, noteID, updated.Content, userID)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, toNoteResponse(updated))
|
||||
}
|
||||
|
||||
@@ -500,3 +522,122 @@ func normalizeFolderPath(p string) string {
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// ── Wikilink Endpoints ──────────────────────
|
||||
|
||||
// GET /api/v1/notes/search-titles?q=query&limit=10
|
||||
func (h *NoteHandler) SearchTitles(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
q := strings.TrimSpace(c.Query("q"))
|
||||
if q == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"})
|
||||
return
|
||||
}
|
||||
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
if limit <= 0 || limit > 50 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
notes, err := h.stores.Notes.SearchTitles(c.Request.Context(), userID, q, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "search failed"})
|
||||
return
|
||||
}
|
||||
|
||||
type titleResult struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
results := make([]titleResult, 0, len(notes))
|
||||
for _, n := range notes {
|
||||
results = append(results, titleResult{ID: n.ID, Title: n.Title})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": results})
|
||||
}
|
||||
|
||||
// GET /api/v1/notes/:id/backlinks
|
||||
func (h *NoteHandler) Backlinks(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
noteID := c.Param("id")
|
||||
|
||||
// Verify ownership
|
||||
note, err := h.stores.Notes.GetByID(c.Request.Context(), noteID)
|
||||
if err != nil || note.UserID != userID {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "note not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if h.stores.NoteLinks == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"data": []interface{}{}})
|
||||
return
|
||||
}
|
||||
|
||||
results, err := h.stores.NoteLinks.Backlinks(c.Request.Context(), noteID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load backlinks"})
|
||||
return
|
||||
}
|
||||
if results == nil {
|
||||
results = []models.NoteLinkResult{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": results})
|
||||
}
|
||||
|
||||
// GET /api/v1/notes/graph
|
||||
func (h *NoteHandler) Graph(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
|
||||
if h.stores.NoteLinks == nil {
|
||||
c.JSON(http.StatusOK, models.NoteGraph{
|
||||
Nodes: []models.NoteGraphNode{},
|
||||
Edges: []models.NoteGraphEdge{},
|
||||
Unresolved: []models.NoteGraphDangling{},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
graph, err := h.stores.NoteLinks.Graph(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load graph"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, graph)
|
||||
}
|
||||
|
||||
// ── Link Extraction Helper ──────────────────
|
||||
|
||||
// extractAndStoreLinks parses [[wikilinks]] from content and stores them.
|
||||
func (h *NoteHandler) extractAndStoreLinks(c *gin.Context, noteID, content, userID string) {
|
||||
if h.stores.NoteLinks == nil {
|
||||
return
|
||||
}
|
||||
|
||||
links := notelinks.ExtractWikilinks(content)
|
||||
if len(links) == 0 {
|
||||
// Clear any existing links
|
||||
h.stores.NoteLinks.ReplaceLinks(c.Request.Context(), noteID, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve titles to note IDs
|
||||
ctx := c.Request.Context()
|
||||
for i, link := range links {
|
||||
notes, err := h.stores.Notes.SearchTitles(ctx, userID, link.TargetTitle, 1)
|
||||
if err == nil {
|
||||
for _, n := range notes {
|
||||
if strings.EqualFold(n.Title, link.TargetTitle) {
|
||||
id := n.ID
|
||||
links[i].TargetNoteID = &id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h.stores.NoteLinks.ReplaceLinks(ctx, noteID, links)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user