Files
nib-v1/internal/db/links.go
lerko e9ecc4c1f7
CI / test (pull_request) Successful in 2m13s
fix: address code review findings across backend and frontend
Fix goroutine-unsafe ULID entropy by wrapping in LockedMonotonicReader.
Move PRAGMA foreign_keys outside transaction in v3 migration where
SQLite was silently ignoring it. Escape LIKE wildcards in link
resolution to prevent false matches. Add non-localhost binding warning,
log writeJSON encoder errors, add ?permanent=true for explicit hard
delete, preserve title/description during absorb, use millisecond
backup timestamps, add path.Clean to spaHandler. Frontend gains
checkedJSON() for resp.ok validation, consistent stopPropagation, and
shared renderCardSections() to eliminate duplicate rendering.
2026-05-21 16:02:57 -04:00

89 lines
2.1 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 escapeLike(s string) string {
r := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`)
return r.Replace(s)
}
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 ? ESCAPE '\' AND id != ? AND deleted_at IS NULL
ORDER BY created_at DESC LIMIT 1`, "%"+escapeLike(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()
}