feat: diagnostics links (#39)

* feat(diagnostic/link): retrive broken links

* add to main

* feat: return array on empty
This commit is contained in:
Kristian 2026-05-09 16:47:59 +02:00 committed by GitHub
parent c0924342fb
commit 70e9f756ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 109 additions and 3 deletions

View file

@ -5,6 +5,7 @@ import (
"os"
"github.com/KristianJBorgwarth/dendrite.daemon/core/handlers/completion"
"github.com/KristianJBorgwarth/dendrite.daemon/core/handlers/diagnostics"
"github.com/KristianJBorgwarth/dendrite.daemon/core/handlers/note"
"github.com/KristianJBorgwarth/dendrite.daemon/core/handlers/vault"
"github.com/KristianJBorgwarth/dendrite.daemon/core/logging"
@ -44,6 +45,8 @@ func main() {
server.RegisterHandler("completion/tag", completion.NewCompleteTagHandler(tagRepo))
server.RegisterHandler("completion/slug", completion.NewCompleteSlugHandler(noteRepo))
server.RegisterHandler("diagnostics/links", diagnostics.NewLinkDiagnosticHandler(noteRepo, linkRepo))
if err := server.Run(os.Stdin, os.Stdout); err != nil {
slog.Error("server error", "error", err)
}

View file

@ -0,0 +1,11 @@
package dtos
type LinkDiagnosticDto struct {
NoteID string `json:"noteId"`
NotePath string `json:"notePath"`
Line int `json:"line"`
Col int `json:"col"`
Raw string `json:"raw"`
Target string `json:"target"`
Message string `json:"message"`
}

View file

@ -0,0 +1,2 @@
// Package diagnostics provides commands for executing diagnostics against vault entities
package diagnostics

View file

@ -0,0 +1,63 @@
package diagnostics
import (
"context"
"encoding/json"
"github.com/KristianJBorgwarth/dendrite.daemon/core/dtos"
"github.com/KristianJBorgwarth/dendrite.daemon/core/models"
"github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories"
)
type LinkDiagnosticCommand struct {
Path string `json:"path"`
}
type LinkDiagnosticHandler struct {
noteRepository repositories.INoteRepository
linkRepository repositories.ILinkRepository
}
func NewLinkDiagnosticHandler(
noteRepository repositories.INoteRepository,
linkRepository repositories.ILinkRepository,
) *LinkDiagnosticHandler {
return &LinkDiagnosticHandler{noteRepository, linkRepository}
}
func (h *LinkDiagnosticHandler) Handle(ctx context.Context, raw json.RawMessage) (any, error) {
var cmd LinkDiagnosticCommand
if err := json.Unmarshal(raw, &cmd); err != nil {
return nil, err
}
note, err := h.noteRepository.GetByPath(ctx, cmd.Path)
if err != nil {
return nil, err
}
brokenLinks, err := h.linkRepository.GetBrokenLinks(ctx, note.ID())
if err != nil {
return nil, err
}
return mapToDiagnosticResult(note, brokenLinks), nil
}
func mapToDiagnosticResult(note *models.Note, links []*models.Link) []*dtos.LinkDiagnosticDto {
diagnostics := make([]*dtos.LinkDiagnosticDto, 0, len(links))
for _, link := range links {
diagnostics = append(diagnostics, &dtos.LinkDiagnosticDto{
NoteID: link.ID(),
NotePath: note.Path(),
Line: link.Line(),
Col: link.Col(),
Raw: link.Raw(),
Target: link.TargetSlug(),
Message: "Broken link: target note does not exist",
})
}
return diagnostics
}

BIN
dendrite

Binary file not shown.

View file

@ -12,10 +12,11 @@ import (
type ILinkRepository interface {
Insert(ctx context.Context, dbContext persistence.IDbContext, links []*models.Link) error
InsertRange(ctx context.Context, dbContext persistence.IDbContext, links []*models.Link) error
GetByNoteID(ctx context.Context, dbContext persistence.IDbContext, fromNoteID string) ([]*models.Link, error)
GetByNoteID(ctx context.Context, fromNoteID string) ([]*models.Link, error)
GetBySlug(ctx context.Context, dbContext persistence.IDbContext, targetSlug string) ([]*models.Link, error)
GetBacklinks(ctx context.Context, slug string) ([]*dtos.BacklinkDto, error)
Delete(ctx context.Context, dbContext persistence.IDbContext, fromNoteID string) error
GetBrokenLinks(ctx context.Context, fromNoteID string) ([]*models.Link, error)
}
type linkRepository struct {
@ -64,8 +65,8 @@ func (r *linkRepository) InsertRange(ctx context.Context, dbContext persistence.
return nil
}
func (r *linkRepository) GetByNoteID(ctx context.Context, dbContext persistence.IDbContext, fromNoteID string) ([]*models.Link, error) {
rows, err := dbContext.QueryContext(ctx, "SELECT id, from_note_id, target_slug, raw, display, line, col FROM link WHERE from_note_id = ?", fromNoteID)
func (r *linkRepository) GetByNoteID(ctx context.Context, fromNoteID string) ([]*models.Link, error) {
rows, err := r.readDBContext.QueryContext(ctx, "SELECT id, from_note_id, target_slug, raw, display, line, col FROM link WHERE from_note_id = ?", fromNoteID)
if err != nil {
return nil, err
}
@ -134,3 +135,29 @@ func (r *linkRepository) Delete(ctx context.Context, dbContext persistence.IDbCo
_, err := dbContext.ExecContext(ctx, "DELETE FROM link WHERE from_note_id = ?", fromNoteID)
return err
}
func (r *linkRepository) GetBrokenLinks(ctx context.Context, fromNoteID string) ([]*models.Link, error) {
query := `
SELECT l.id, l.from_note_id, l.target_slug, l.raw, l.display, l.line, l.col
FROM link l
LEFT JOIN note n ON l.target_slug = n.slug
WHERE l.from_note_id = ? AND n.id IS NULL`
rows, err := r.readDBContext.QueryContext(ctx, query, fromNoteID)
if err != nil {
return nil, err
}
defer rows.Close()
var brokenLinks []*models.Link
for rows.Next() {
var id, rowFromNoteID, targetSlug, raw, display string
var line, col int
if err := rows.Scan(&id, &rowFromNoteID, &targetSlug, &raw, &display, &line, &col); err != nil {
return nil, err
}
brokenLinks = append(brokenLinks, models.NewLink(id, fromNoteID, targetSlug, raw, display, line, col))
}
return brokenLinks, nil
}