feat(note/backlinks): added support for querying backlinks (#33)

* feat(link/backlinks): backlinks query

* feat(link/backlinks): register handler

* feat(note/backlinks): added backlink query and handler
This commit is contained in:
Kristian 2026-04-26 14:28:34 +02:00 committed by GitHub
parent 594764344d
commit fda0286690
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 111 additions and 0 deletions

View file

@ -37,6 +37,7 @@ func main() {
server.RegisterHandler("note/create", note.NewCreateNoteHandler(uow, tagService, noteRepo))
server.RegisterHandler("note/save", note.NewSaveNoteHandler(uow, noteRepo, tagService, noteService, linkService))
server.RegisterHandler("note/goto", note.NewGotoNoteHandler(noteRepo))
server.RegisterHandler("note/backlinks", note.NewGetBackLinksHandler(linkRepo, noteRepo))
server.RegisterHandler("completion/link", completion.NewCompleteLinkHandler(linkRepo))
server.RegisterHandler("completion/tag", completion.NewCompleteTagHandler(tagRepo))

23
core/dtos/backlink.go Normal file
View file

@ -0,0 +1,23 @@
package dtos
type Backlink struct {
ID string `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Path string `json:"path"`
Raw string `json:"raw"`
Line int `json:"line"`
Col int `json:"col"`
}
func NewBacklink(id, title, slug, path, raw string, col, line int) *Backlink {
return &Backlink{
ID: id,
Title: title,
Slug: slug,
Path: path,
Raw: raw,
Line: line,
Col: col,
}
}

2
core/dtos/doc.go Normal file
View file

@ -0,0 +1,2 @@
// Package dtos contains the data transfer objects used in the application.
package dtos

View file

@ -0,0 +1,40 @@
package note
import (
"context"
"encoding/json"
"github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories"
)
type getBacklinksCommand struct {
Path string `json:"path"`
}
type GetBackLinksHandler struct {
linkRepo repositories.ILinkRepository
noteRepo repositories.INoteRepository
}
func NewGetBackLinksHandler(lr repositories.ILinkRepository, nr repositories.INoteRepository) *GetBackLinksHandler {
return &GetBackLinksHandler{linkRepo: lr, noteRepo: nr}
}
func (h *GetBackLinksHandler) Handle(ctx context.Context, raw json.RawMessage) (any, error) {
var cmd getBacklinksCommand
if err := json.Unmarshal(raw, &cmd); err != nil {
return nil, err
}
note, err := h.noteRepo.GetByPath(ctx, cmd.Path)
if err != nil {
return nil, err
}
links, err := h.linkRepo.GetBacklinks(ctx, note.Slug())
if err != nil {
return nil, err
}
return links, nil
}

BIN
dendrite

Binary file not shown.

View file

@ -4,6 +4,7 @@ import (
"context"
"strings"
"github.com/KristianJBorgwarth/dendrite.daemon/core/dtos"
"github.com/KristianJBorgwarth/dendrite.daemon/core/models"
"github.com/KristianJBorgwarth/dendrite.daemon/persistence"
)
@ -14,6 +15,7 @@ type ILinkRepository interface {
GetByNoteID(ctx context.Context, dbContext persistence.IDbContext, fromNoteID string) ([]*models.Link, error)
GetBySlug(ctx context.Context, dbContext persistence.IDbContext, targetSlug string) ([]*models.Link, error)
Search(ctx context.Context, query string) ([]*models.Link, error)
GetBacklinks(ctx context.Context, slug string) ([]*dtos.Backlink, error)
Delete(ctx context.Context, dbContext persistence.IDbContext, fromNoteID string) error
}
@ -123,6 +125,32 @@ func (r *linkRepository) Search(ctx context.Context, query string) ([]*models.Li
return links, nil
}
func (r *linkRepository) GetBacklinks(ctx context.Context, slug string) ([]*dtos.Backlink, error) {
query := `
SELECT n.id, n.slug, n.title, n.path, l.raw, l.line, l.col
FROM link l
JOIN note n ON l.from_note_id = n.id
WHERE l.target_slug = ?`
rows, err := r.readDBContext.QueryContext(ctx, query, slug)
if err != nil {
return nil, err
}
defer rows.Close()
backlinks := make([]*dtos.Backlink, 0)
for rows.Next() {
var noteID, noteSlug, noteTitle, notePath, raw string
var line, col int
if err := rows.Scan(&noteID, &noteSlug, &noteTitle, &notePath, &raw, &line, &col); err != nil {
return nil, err
}
backlinks = append(backlinks, dtos.NewBacklink(noteID, noteTitle, noteSlug, notePath, raw, line, col))
}
return backlinks, nil
}
func (r *linkRepository) Delete(ctx context.Context, dbContext persistence.IDbContext, fromNoteID string) error {
_, err := dbContext.ExecContext(ctx, "DELETE FROM link WHERE from_note_id = ?", fromNoteID)
return err

View file

@ -14,6 +14,7 @@ type INoteRepository interface {
InsertRange(ctx context.Context, dbContext persistence.IDbContext, note []*models.Note) error
Update(ctx context.Context, dbContext persistence.IDbContext, noteID, path, title, slug string) error
GetBySlug(ctx context.Context, slug string) (*models.Note, error)
GetByPath(ctx context.Context, path string) (*models.Note, error)
GetNoteCount(ctx context.Context) (int, error)
}
@ -84,6 +85,22 @@ func (r *noteRepository) GetBySlug(ctx context.Context, slug string) (*models.No
return models.NewNote(id, path, title, slug, createdAt, updatedAt), nil
}
func (r *noteRepository) GetByPath(ctx context.Context, path string) (*models.Note, error) {
query := `SELECT id, title, path, slug, created_at, updated_at FROM note WHERE path = ?`
row := r.readDBContext.QueryRowContext(ctx, query, path)
var id, title, slug, createdAt, updatedAt string
err := row.Scan(&id, &title, &path, &slug, &createdAt, &updatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return models.NewNote(id, path, title, slug, createdAt, updatedAt), nil
}
func (r *noteRepository) GetNoteCount(ctx context.Context) (int, error) {
query := `SELECT COUNT(*) FROM note`
row := r.readDBContext.QueryRowContext(ctx, query)