1e58433936
CI / test (pull_request) Successful in 2m27s
[[wiki-links]] in entry bodies are extracted at save time, resolved to entity IDs (title match first, body substring fallback), and stored in entity_links junction table. Backlinks surface in TUI detail view showing entries that link to the current entry. Schema migration v5 adds entity_links with CASCADE/SET NULL semantics. Links sync on Create, Update, and Absorb.
84 lines
2.0 KiB
Go
84 lines
2.0 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"strings"
|
|
|
|
"github.com/lerko/nib/internal/link"
|
|
)
|
|
|
|
type Backlink struct {
|
|
EntityID string
|
|
Title *string
|
|
Body string
|
|
LinkText string
|
|
}
|
|
|
|
func (s *Store) resolveLink(ctx context.Context, tx *sql.Tx, linkText string, excludeID string) *string {
|
|
lower := strings.ToLower(linkText)
|
|
|
|
var id string
|
|
err := tx.QueryRowContext(ctx, `
|
|
SELECT id FROM entities
|
|
WHERE LOWER(title) = ? AND id != ? AND deleted_at IS NULL
|
|
ORDER BY created_at DESC LIMIT 1`, lower, excludeID).Scan(&id)
|
|
if err == nil {
|
|
return &id
|
|
}
|
|
|
|
err = tx.QueryRowContext(ctx, `
|
|
SELECT id FROM entities
|
|
WHERE LOWER(body) LIKE ? AND id != ? AND deleted_at IS NULL
|
|
ORDER BY created_at DESC LIMIT 1`, "%"+lower+"%", excludeID).Scan(&id)
|
|
if err == nil {
|
|
return &id
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func syncLinks(ctx context.Context, tx *sql.Tx, s *Store, entityID string, body string) error {
|
|
if _, err := tx.ExecContext(ctx, "DELETE FROM entity_links WHERE from_id = ?", entityID); err != nil {
|
|
return err
|
|
}
|
|
|
|
linkTexts := link.ExtractLinks(body)
|
|
for _, lt := range linkTexts {
|
|
toID := s.resolveLink(ctx, tx, lt, entityID)
|
|
if _, err := tx.ExecContext(ctx,
|
|
"INSERT OR IGNORE INTO entity_links (from_id, to_id, link_text) VALUES (?, ?, ?)",
|
|
entityID, toID, lt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) LoadBacklinks(ctx context.Context, entityID string) ([]Backlink, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT e.id, e.title, e.body, el.link_text
|
|
FROM entity_links el
|
|
JOIN entities e ON e.id = el.from_id
|
|
WHERE el.to_id = ? AND e.deleted_at IS NULL
|
|
ORDER BY e.created_at DESC`, entityID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var backlinks []Backlink
|
|
for rows.Next() {
|
|
var bl Backlink
|
|
var title sql.NullString
|
|
if err := rows.Scan(&bl.EntityID, &title, &bl.Body, &bl.LinkText); err != nil {
|
|
return nil, err
|
|
}
|
|
if title.Valid {
|
|
bl.Title = &title.String
|
|
}
|
|
backlinks = append(backlinks, bl)
|
|
}
|
|
return backlinks, rows.Err()
|
|
}
|