diff --git a/cmd/main.go b/cmd/main.go index cea50d4..acc6d09 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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) } diff --git a/core/dtos/link_diagnostic_dto.go b/core/dtos/link_diagnostic_dto.go new file mode 100644 index 0000000..76ff124 --- /dev/null +++ b/core/dtos/link_diagnostic_dto.go @@ -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"` +} diff --git a/core/handlers/diagnostics/doc.go b/core/handlers/diagnostics/doc.go new file mode 100644 index 0000000..6f574a3 --- /dev/null +++ b/core/handlers/diagnostics/doc.go @@ -0,0 +1,2 @@ +// Package diagnostics provides commands for executing diagnostics against vault entities +package diagnostics diff --git a/core/handlers/diagnostics/links_diagnostic_handler.go b/core/handlers/diagnostics/links_diagnostic_handler.go new file mode 100644 index 0000000..8be0f1c --- /dev/null +++ b/core/handlers/diagnostics/links_diagnostic_handler.go @@ -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 +} diff --git a/dendrite b/dendrite index 3a23b1f..87661a1 100755 Binary files a/dendrite and b/dendrite differ diff --git a/persistence/repositories/link_repository.go b/persistence/repositories/link_repository.go index 9645d4a..5933b61 100644 --- a/persistence/repositories/link_repository.go +++ b/persistence/repositories/link_repository.go @@ -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 +}