- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
183 lines
4.8 KiB
Go
183 lines
4.8 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
|
|
"switchboard-core/models"
|
|
)
|
|
|
|
type NoteLinkStore struct{}
|
|
|
|
func NewNoteLinkStore() *NoteLinkStore { return &NoteLinkStore{} }
|
|
|
|
func (s *NoteLinkStore) ReplaceLinks(ctx context.Context, sourceNoteID string, links []models.NoteLink) error {
|
|
tx, err := DB.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Delete existing links for this source note
|
|
if _, err := tx.ExecContext(ctx,
|
|
"DELETE FROM note_links WHERE source_note_id = ?", sourceNoteID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Insert new links
|
|
if len(links) > 0 {
|
|
stmt, err := tx.PrepareContext(ctx, `
|
|
INSERT OR IGNORE INTO note_links (source_note_id, target_note_id, target_title, display_text, is_transclusion)
|
|
VALUES (?, ?, ?, ?, ?)`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stmt.Close()
|
|
|
|
for _, l := range links {
|
|
var targetID interface{}
|
|
if l.TargetNoteID != nil {
|
|
targetID = *l.TargetNoteID
|
|
}
|
|
isTransclusion := 0
|
|
if l.IsTransclusion {
|
|
isTransclusion = 1
|
|
}
|
|
if _, err := stmt.ExecContext(ctx,
|
|
sourceNoteID, targetID, l.TargetTitle, l.DisplayText, isTransclusion,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (s *NoteLinkStore) ResolveByTitle(ctx context.Context, userID, targetNoteID, title string) error {
|
|
_, err := DB.ExecContext(ctx, `
|
|
UPDATE note_links
|
|
SET target_note_id = ?
|
|
WHERE target_note_id IS NULL
|
|
AND LOWER(target_title) = LOWER(?)
|
|
AND source_note_id IN (SELECT id FROM notes WHERE user_id = ?)`,
|
|
targetNoteID, title, userID)
|
|
return err
|
|
}
|
|
|
|
func (s *NoteLinkStore) Backlinks(ctx context.Context, noteID string) ([]models.NoteLinkResult, error) {
|
|
rows, err := DB.QueryContext(ctx, `
|
|
SELECT n.id, n.title, n.folder_path, n.updated_at, COALESCE(nl.display_text, '')
|
|
FROM note_links nl
|
|
JOIN notes n ON n.id = nl.source_note_id
|
|
WHERE nl.target_note_id = ?
|
|
ORDER BY n.updated_at DESC`, noteID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var results []models.NoteLinkResult
|
|
for rows.Next() {
|
|
var r models.NoteLinkResult
|
|
if err := rows.Scan(&r.SourceNoteID, &r.Title, &r.FolderPath,
|
|
st(&r.UpdatedAt), &r.DisplayText); err != nil {
|
|
return nil, err
|
|
}
|
|
results = append(results, r)
|
|
}
|
|
return results, rows.Err()
|
|
}
|
|
|
|
func (s *NoteLinkStore) Graph(ctx context.Context, userID string) (*models.NoteGraph, error) {
|
|
graph := &models.NoteGraph{
|
|
Nodes: make([]models.NoteGraphNode, 0),
|
|
Edges: make([]models.NoteGraphEdge, 0),
|
|
Unresolved: make([]models.NoteGraphDangling, 0),
|
|
}
|
|
|
|
// Nodes: all user's notes with link counts
|
|
nodeRows, err := DB.QueryContext(ctx, `
|
|
SELECT n.id, n.title, n.folder_path, n.tags, n.updated_at,
|
|
COALESCE(outbound.cnt, 0) + COALESCE(inbound.cnt, 0) AS link_count
|
|
FROM notes n
|
|
LEFT JOIN (
|
|
SELECT source_note_id, COUNT(*) AS cnt FROM note_links
|
|
WHERE target_note_id IS NOT NULL GROUP BY source_note_id
|
|
) outbound ON outbound.source_note_id = n.id
|
|
LEFT JOIN (
|
|
SELECT target_note_id, COUNT(*) AS cnt FROM note_links
|
|
WHERE target_note_id IS NOT NULL GROUP BY target_note_id
|
|
) inbound ON inbound.target_note_id = n.id
|
|
WHERE n.user_id = ?
|
|
ORDER BY n.updated_at DESC`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer nodeRows.Close()
|
|
|
|
for nodeRows.Next() {
|
|
var node models.NoteGraphNode
|
|
var tagsJSON string
|
|
if err := nodeRows.Scan(&node.ID, &node.Title, &node.FolderPath,
|
|
&tagsJSON, &node.UpdatedAt, &node.LinkCount); err != nil {
|
|
return nil, err
|
|
}
|
|
node.Tags = ScanArray(tagsJSON)
|
|
if node.Tags == nil {
|
|
node.Tags = []string{}
|
|
}
|
|
graph.Nodes = append(graph.Nodes, node)
|
|
}
|
|
if err := nodeRows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Resolved edges
|
|
edgeRows, err := DB.QueryContext(ctx, `
|
|
SELECT nl.source_note_id, nl.target_note_id, nl.target_title, nl.is_transclusion
|
|
FROM note_links nl
|
|
JOIN notes n ON n.id = nl.source_note_id
|
|
WHERE n.user_id = ?
|
|
AND nl.target_note_id IS NOT NULL`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer edgeRows.Close()
|
|
|
|
for edgeRows.Next() {
|
|
var edge models.NoteGraphEdge
|
|
var isTransclusion int
|
|
if err := edgeRows.Scan(&edge.Source, &edge.Target,
|
|
&edge.Title, &isTransclusion); err != nil {
|
|
return nil, err
|
|
}
|
|
edge.IsTransclusion = isTransclusion != 0
|
|
graph.Edges = append(graph.Edges, edge)
|
|
}
|
|
if err := edgeRows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Unresolved links (dangling)
|
|
danglingRows, err := DB.QueryContext(ctx, `
|
|
SELECT nl.source_note_id, nl.target_title
|
|
FROM note_links nl
|
|
JOIN notes n ON n.id = nl.source_note_id
|
|
WHERE n.user_id = ?
|
|
AND nl.target_note_id IS NULL`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer danglingRows.Close()
|
|
|
|
for danglingRows.Next() {
|
|
var d models.NoteGraphDangling
|
|
if err := danglingRows.Scan(&d.Source, &d.Title); err != nil {
|
|
return nil, err
|
|
}
|
|
graph.Unresolved = append(graph.Unresolved, d)
|
|
}
|
|
|
|
return graph, danglingRows.Err()
|
|
}
|