From 865430d21c43ead9534835579afda26403072b9e Mon Sep 17 00:00:00 2001 From: Kristian <10348902@pm.me> Date: Sun, 31 May 2026 12:45:18 +0200 Subject: [PATCH] feat(cfe): get notes by cfe (#46) --- cmd/main.go | 1 + core/dtos/note_dto.go | 8 ++ .../handlers/note/get_notes_by_cfe_handler.go | 47 ++++++++++++ .../handlers/note/get_notes_by_tag_handler.go | 6 +- persistence/repositories/cfe_repository.go | 32 ++++++++ .../get_notes_by_cfe_handler_test.go | 74 +++++++++++++++++++ test/test_integration/main_test.go | 2 - test/test_integration/utils/DB_cfe_utils.go | 32 ++++++++ test/test_integration/utils/DB_note_utils.go | 30 ++++++++ test/test_integration/utils/doc.go | 2 + 10 files changed, 228 insertions(+), 6 deletions(-) create mode 100644 core/handlers/note/get_notes_by_cfe_handler.go create mode 100644 test/test_integration/get_notes_by_cfe_handler_test.go create mode 100644 test/test_integration/utils/DB_cfe_utils.go create mode 100644 test/test_integration/utils/DB_note_utils.go create mode 100644 test/test_integration/utils/doc.go diff --git a/cmd/main.go b/cmd/main.go index 9bfb1c8..e431377 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -43,6 +43,7 @@ func main() { server.RegisterHandler("note/goto", note.NewGotoNoteHandler(noteRepo)) server.RegisterHandler("note/backlinks", note.NewGetBackLinksHandler(linkRepo, noteRepo)) server.RegisterHandler("note/search_by_tag", note.NewGetNotesByTagHandler(noteRepo)) + server.RegisterHandler("note/search_by_cfe", note.NewGetNotesByCfeHandler(cfeRepo)) server.RegisterHandler("completion/tag", completion.NewCompleteTagHandler(tagRepo)) server.RegisterHandler("completion/slug", completion.NewCompleteSlugHandler(noteRepo)) diff --git a/core/dtos/note_dto.go b/core/dtos/note_dto.go index df4ce80..296d573 100644 --- a/core/dtos/note_dto.go +++ b/core/dtos/note_dto.go @@ -17,3 +17,11 @@ func NewNoteDto(note *models.Note) *NoteDto { Slug: note.Slug(), } } + +func NewNoteDtos(notes []*models.Note) []*NoteDto { + dtos := make([]*NoteDto, len(notes)) + for i, note := range notes { + dtos[i] = NewNoteDto(note) + } + return dtos +} diff --git a/core/handlers/note/get_notes_by_cfe_handler.go b/core/handlers/note/get_notes_by_cfe_handler.go new file mode 100644 index 0000000..6de0d5b --- /dev/null +++ b/core/handlers/note/get_notes_by_cfe_handler.go @@ -0,0 +1,47 @@ +package note + +import ( + "context" + "encoding/json" + + "github.com/KristianJBorgwarth/dendrite.daemon/core/dtos" + "github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories" +) + +type getNotesByCfeQuery struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type GetNotesByCfeHandler struct { + cfeRepo repositories.ICfeRepository +} + +func NewGetNotesByCfeHandler( + cfeRepo repositories.ICfeRepository, +) *GetNotesByCfeHandler { + return &GetNotesByCfeHandler{cfeRepo: cfeRepo} +} + +func (h *GetNotesByCfeHandler) Handle( + ctx context.Context, + raw json.RawMessage, +) (any, error) { + var query getNotesByCfeQuery + if err := json.Unmarshal(raw, &query); err != nil { + return nil, err + } + + notes, err := h.cfeRepo.Search(ctx, query.Key, query.Value) + if err != nil { + return nil, err + } + + if len(notes) == 0 { + return make([]*dtos.NoteDto,0), nil + } + + noteDtos := dtos.NewNoteDtos(notes) + + return noteDtos, nil +} diff --git a/core/handlers/note/get_notes_by_tag_handler.go b/core/handlers/note/get_notes_by_tag_handler.go index ebc9b5a..5cd16ae 100644 --- a/core/handlers/note/get_notes_by_tag_handler.go +++ b/core/handlers/note/get_notes_by_tag_handler.go @@ -7,6 +7,7 @@ import ( "github.com/KristianJBorgwarth/dendrite.daemon/core/dtos" "github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories" ) + type getNotesByTagCommand struct { Tag string `json:"tag"` } @@ -30,10 +31,7 @@ func (h *GetNotesByTagHandler) Handle(ctx context.Context, raw json.RawMessage) return nil, err } - noteDtos := make([]*dtos.NoteDto, len(notes)) - for i, note := range notes { - noteDtos[i] = dtos.NewNoteDto(note) - } + noteDtos := dtos.NewNoteDtos(notes) return noteDtos, nil } diff --git a/persistence/repositories/cfe_repository.go b/persistence/repositories/cfe_repository.go index bc9f59c..18356e4 100644 --- a/persistence/repositories/cfe_repository.go +++ b/persistence/repositories/cfe_repository.go @@ -9,6 +9,7 @@ import ( type ICfeRepository interface { InsertRange(ctx context.Context, dbContext persistence.IDbContext, cfe []*models.CustomFronMatter) error + Search(ctx context.Context, key string, value string) ([]*models.Note, error) Delete(ctx context.Context, dbContext persistence.IDbContext, noteID string) error } @@ -57,3 +58,34 @@ func (r *cfeRepository) Delete( return nil } + +func (r *cfeRepository) Search( + ctx context.Context, + key string, + value string, +) ([]*models.Note, error) { + rows, err := r.readDBContext.QueryContext( + ctx, + `SELECT n.id, n.path, n.title, n.slug, n.created_at, n.updated_at + FROM note n + INNER JOIN custom_frontmatter cfe ON n.id = cfe.note_id + WHERE cfe.key = ? AND cfe.value LIKE ?;`, + key, + "%"+value+"%", + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var notes []*models.Note + for rows.Next() { + var id, title, path, slug, createdAt, updatedAT string + if err := rows.Scan(&id, &title, &path, &slug, &createdAt, &updatedAT); err != nil { + return nil, err + } + notes = append(notes, models.NewNote(id, title, path, slug, createdAt, updatedAT)) + } + + return notes, nil +} diff --git a/test/test_integration/get_notes_by_cfe_handler_test.go b/test/test_integration/get_notes_by_cfe_handler_test.go new file mode 100644 index 0000000..4dabdcf --- /dev/null +++ b/test/test_integration/get_notes_by_cfe_handler_test.go @@ -0,0 +1,74 @@ +package integration_test + +import ( + "encoding/json" + "testing" + + "github.com/KristianJBorgwarth/dendrite.daemon/core/dtos" + "github.com/KristianJBorgwarth/dendrite.daemon/core/handlers/note" + "github.com/KristianJBorgwarth/dendrite.daemon/persistence" + "github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories" + "github.com/KristianJBorgwarth/dendrite.daemon/test/test_integration/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newGetNotesByCfeHandler() *note.GetNotesByCfeHandler { + return note.NewGetNotesByCfeHandler( + repositories.NewCfeRepository(persistence.NewReadContext()), + ) +} + +func Test_Handle_ReturnsNotesWithMacthingCfe(t *testing.T) { + // Arrange + handler := newGetNotesByCfeHandler() + noteID := "test-note-id" + cfeKey := "author" + cfeValues := []string{"John Doe", "Jane Smith"} + utils.CreateNote(Fixture.TestContext, Fixture.DB, noteID, "Test Note", "test-note.md", "test"); + + utils.CreateCfe(Fixture.TestContext, Fixture.DB, noteID, + cfeKey, cfeValues) + + params, _ := json.Marshal(map[string]any{ + "key": "author", + "value": "J", + }) + + // Act + result, err := handler.Handle(Fixture.TestContext, params) + + require.NoError(t, err) + require.NotNil(t, result) + + noteDtos, ok := result.([]*dtos.NoteDto) + require.True(t, ok) + assert.Len(t, noteDtos, 2) +} + +func Test_Handle_ReturnsEmptyListWhenNoMatchingCfe(t *testing.T) { + // Arrange + handler := newGetNotesByCfeHandler() + noteID := "test-note-id-2" + cfeKey := "category" + cfeValues := []string{"Tech", "Lifestyle"} + utils.CreateNote(Fixture.TestContext, Fixture.DB, noteID, "Another Test Note", "another-test-note.md", "test"); + + utils.CreateCfe(Fixture.TestContext, Fixture.DB, noteID, + cfeKey, cfeValues) + + params, _ := json.Marshal(map[string]any{ + "key": "category", + "value": "NonExistingCategory", + }) + + // Act + result, err := handler.Handle(Fixture.TestContext, params) + + require.NoError(t, err) + require.NotNil(t, result) + + noteDtos, ok := result.([]*dtos.NoteDto) + require.True(t, ok) + assert.Len(t, noteDtos, 0) +} diff --git a/test/test_integration/main_test.go b/test/test_integration/main_test.go index 0261633..5af9bb8 100644 --- a/test/test_integration/main_test.go +++ b/test/test_integration/main_test.go @@ -54,9 +54,7 @@ var Fixture = NewDBFixture() func TestMain(m *testing.M) { code := m.Run() - os.RemoveAll(Fixture.DBPath) - os.Exit(code) } diff --git a/test/test_integration/utils/DB_cfe_utils.go b/test/test_integration/utils/DB_cfe_utils.go new file mode 100644 index 0000000..4c4e771 --- /dev/null +++ b/test/test_integration/utils/DB_cfe_utils.go @@ -0,0 +1,32 @@ +package utils + +import ( + "context" + + "github.com/KristianJBorgwarth/dendrite.daemon/core/models" + "github.com/KristianJBorgwarth/dendrite.daemon/persistence" +) + +func CreateCfe( + ctx context.Context, + dbCtx persistence.IDbContext, + noteID, key string, + values []string, +) error { + for _, value := range values { + cfe := models.NewCustomFrontMatter(noteID, key, value) + statement, err := dbCtx.Prepare( + `INSERT INTO custom_frontmatter (note_id, key, value) + VALUES (?, ?, ?) ON CONFLICT DO NOTHING;`) + if err != nil { + return err + } + + if _, err := statement.ExecContext(ctx, cfe.NodeID(), cfe.Key(), cfe.Value()); err != nil { + return err + } + } + return nil +} + + diff --git a/test/test_integration/utils/DB_note_utils.go b/test/test_integration/utils/DB_note_utils.go new file mode 100644 index 0000000..e79d1a2 --- /dev/null +++ b/test/test_integration/utils/DB_note_utils.go @@ -0,0 +1,30 @@ +package utils + +import ( + "context" + + "github.com/KristianJBorgwarth/dendrite.daemon/persistence" +) + +func CreateNote( + ctx context.Context, + dbCtx persistence.IDbContext, + noteID, + path, + title, + slug string, +) error { + statement, err := dbCtx.Prepare(` + INSERT INTO note (id, title, path, slug, created_at, updated_at) + VALUES (?, ?, ?, ?, datetime('now'), datetime('now')) + ON CONFLICT DO NOTHING;`) + if err != nil { + return err + } + + if _, err := statement.ExecContext(ctx, noteID, title, path, slug); err != nil { + return err + } + + return nil +} diff --git a/test/test_integration/utils/doc.go b/test/test_integration/utils/doc.go new file mode 100644 index 0000000..417aa4d --- /dev/null +++ b/test/test_integration/utils/doc.go @@ -0,0 +1,2 @@ +// Package utils provides testing utilities for integration tests +package utils