Compare commits

..

10 commits

Author SHA1 Message Date
4025f03826
template path fix (#48) 2026-06-10 19:58:03 +02:00
a21ff37225 update install script 2026-06-09 23:13:05 +02:00
86ac6b33a4
cleanup (#47) 2026-05-31 21:15:12 +02:00
865430d21c
feat(cfe): get notes by cfe (#46) 2026-05-31 12:45:18 +02:00
2f94782d33
feat(cfe): add cfe on save and rebuild (#45) 2026-05-30 23:33:30 +02:00
87536b474a
feat(cfe): add cfe on create (#44) 2026-05-30 22:14:44 +02:00
f1bc87e3ad
feat: add cfm model and migration (#43)
* custom fe matter migration

* add custom_fe_matter
2026-05-30 13:12:22 +02:00
d702e935b5 chore: untrack dendrite binary 2026-05-30 11:49:39 +02:00
fdb7ceb190 gitignore 2026-05-30 11:44:02 +02:00
0eeb588983
ref(cfg): extend init command with additional props (#42)
* feat(v_config): changed vault configuration

* ref(cfg): extend init cmd with config props
2026-05-27 21:01:58 +02:00
28 changed files with 555 additions and 89 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
dendrite

View file

@ -26,21 +26,24 @@ func main() {
linkRepo := repositories.NewLinkRepository(persistence.NewReadContext()) linkRepo := repositories.NewLinkRepository(persistence.NewReadContext())
tagRepo := repositories.NewTagRepository(persistence.NewReadContext()) tagRepo := repositories.NewTagRepository(persistence.NewReadContext())
noteRepo := repositories.NewNoteRepository(persistence.NewReadContext()) noteRepo := repositories.NewNoteRepository(persistence.NewReadContext())
cfeRepo := repositories.NewCfeRepository(persistence.NewReadContext())
tagService := services.NewTagService(tagRepo) tagService := services.NewTagService(tagRepo)
linkService := services.NewLinkService(linkRepo) linkService := services.NewLinkService(linkRepo)
noteService := services.NewNoteService(tagRepo, linkRepo, noteRepo) noteService := services.NewNoteService(tagRepo, linkRepo, noteRepo, cfeRepo)
idxr := services.NewIndexRebuilder(uow, noteRepo, linkRepo, tagRepo, indexRepo ) cfeSvc := services.NewCfeService(cfeRepo)
idxr := services.NewIndexRebuilder(uow, noteRepo, linkRepo, tagRepo, indexRepo, cfeRepo)
server.RegisterHandler("vault/init", vault.NewInitializeHandler(idxr, noteService)) server.RegisterHandler("vault/init", vault.NewInitializeHandler(idxr, noteService))
server.RegisterHandler("vault/rebuild", vault.NewRebuildIndexHandler(idxr)) server.RegisterHandler("vault/rebuild", vault.NewRebuildIndexHandler(idxr))
server.RegisterHandler("note/create", note.NewCreateNoteHandler(uow, tagService, noteRepo)) server.RegisterHandler("note/create", note.NewCreateNoteHandler(uow, tagService, noteRepo, cfeSvc))
server.RegisterHandler("note/save", note.NewSaveNoteHandler(uow, noteRepo, tagService, noteService, linkService)) server.RegisterHandler("note/save", note.NewSaveNoteHandler(uow, noteRepo, tagService, noteService, linkService, cfeSvc))
server.RegisterHandler("note/delete", note.NewDeleteNoteHandler(uow, tagService, noteService)) server.RegisterHandler("note/delete", note.NewDeleteNoteHandler(uow, tagService, noteService))
server.RegisterHandler("note/goto", note.NewGotoNoteHandler(noteRepo)) server.RegisterHandler("note/goto", note.NewGotoNoteHandler(noteRepo))
server.RegisterHandler("note/backlinks", note.NewGetBackLinksHandler(linkRepo, noteRepo)) server.RegisterHandler("note/backlinks", note.NewGetBackLinksHandler(linkRepo, noteRepo))
server.RegisterHandler("note/search_by_tag", note.NewGetNotesByTagHandler(noteRepo)) server.RegisterHandler("note/search_by_tag", note.NewGetNotesByTagHandler(noteRepo))
server.RegisterHandler("note/search_by_cf", note.NewGetNotesByCfeHandler(cfeRepo))
server.RegisterHandler("completion/tag", completion.NewCompleteTagHandler(tagRepo)) server.RegisterHandler("completion/tag", completion.NewCompleteTagHandler(tagRepo))
server.RegisterHandler("completion/slug", completion.NewCompleteSlugHandler(noteRepo)) server.RegisterHandler("completion/slug", completion.NewCompleteSlugHandler(noteRepo))

View file

@ -17,3 +17,11 @@ func NewNoteDto(note *models.Note) *NoteDto {
Slug: note.Slug(), 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
}

View file

@ -7,12 +7,12 @@ import (
) )
type FrontMatter struct { type FrontMatter struct {
Title string `yaml:"title"` Title string `yaml:"title"`
Tags []string `yaml:"tags"` Tags []string `yaml:"tags"`
Created string `yaml:"created"` Created string `yaml:"created"`
Updated string `yaml:"updated"` Updated string `yaml:"updated"`
Date string `yaml:"date"` Date string `yaml:"date"`
Author string `yaml:"author"` Custom map[string]any `yaml:",inline"`
} }
func ParseFrontMatter(file []byte) (*FrontMatter, error) { func ParseFrontMatter(file []byte) (*FrontMatter, error) {

View file

@ -20,16 +20,18 @@ type createNoteCommand struct {
type CreateNoteHandler struct { type CreateNoteHandler struct {
uow *repositories.UnitOfWork uow *repositories.UnitOfWork
tagService services.ITagService tagSvc services.ITagService
noteRepo repositories.INoteRepository noteRepo repositories.INoteRepository
cfeSvc services.ICfService
} }
func NewCreateNoteHandler( func NewCreateNoteHandler(
uow *repositories.UnitOfWork, uow *repositories.UnitOfWork,
tagRepo services.ITagService, tagSvc services.ITagService,
noteRepo repositories.INoteRepository, noteRepo repositories.INoteRepository,
cfeSvc services.ICfService,
) *CreateNoteHandler { ) *CreateNoteHandler {
return &CreateNoteHandler{uow, tagRepo, noteRepo} return &CreateNoteHandler{uow, tagSvc, noteRepo, cfeSvc}
} }
func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any, error) { func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any, error) {
@ -62,7 +64,7 @@ func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (an
return notePath, nil return notePath, nil
} }
tagModels, err := h.tagService.CreateTags(ctx, dbCtx, template.FrontMatter.Tags) tagModels, err := h.tagSvc.CreateTags(ctx, dbCtx, template.FrontMatter.Tags)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -73,7 +75,11 @@ func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (an
return nil, err return nil, err
} }
if err = h.tagService.CreateNoteTags(ctx, dbCtx, note.ID(), tagModels); err != nil { if err = h.tagSvc.CreateNoteTags(ctx, dbCtx, note.ID(), tagModels); err != nil {
return nil, err
}
if err = h.cfeSvc.AddCfe(ctx, dbCtx, note.ID(), template.FrontMatter.Custom); err != nil {
return nil, err return nil, err
} }

View file

@ -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 getNotesByCfQuery struct {
Key string `json:"key"`
Value string `json:"value"`
}
type GetNotesByCfHandler struct {
cfeRepo repositories.ICfRepository
}
func NewGetNotesByCfeHandler(
cfeRepo repositories.ICfRepository,
) *GetNotesByCfHandler {
return &GetNotesByCfHandler{cfeRepo: cfeRepo}
}
func (h *GetNotesByCfHandler) Handle(
ctx context.Context,
raw json.RawMessage,
) (any, error) {
var query getNotesByCfQuery
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
}

View file

@ -7,6 +7,7 @@ import (
"github.com/KristianJBorgwarth/dendrite.daemon/core/dtos" "github.com/KristianJBorgwarth/dendrite.daemon/core/dtos"
"github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories" "github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories"
) )
type getNotesByTagCommand struct { type getNotesByTagCommand struct {
Tag string `json:"tag"` Tag string `json:"tag"`
} }
@ -30,10 +31,7 @@ func (h *GetNotesByTagHandler) Handle(ctx context.Context, raw json.RawMessage)
return nil, err return nil, err
} }
noteDtos := make([]*dtos.NoteDto, len(notes)) noteDtos := dtos.NewNoteDtos(notes)
for i, note := range notes {
noteDtos[i] = dtos.NewNoteDto(note)
}
return noteDtos, nil return noteDtos, nil
} }

View file

@ -20,6 +20,7 @@ type SaveNoteHandler struct {
tagService services.ITagService tagService services.ITagService
noteService services.INoteService noteService services.INoteService
linkService services.ILinkService linkService services.ILinkService
cfeSvc services.ICfService
} }
func NewSaveNoteHandler( func NewSaveNoteHandler(
@ -28,8 +29,9 @@ func NewSaveNoteHandler(
ts services.ITagService, ts services.ITagService,
ns services.INoteService, ns services.INoteService,
ls services.ILinkService, ls services.ILinkService,
cfs services.ICfService,
) *SaveNoteHandler { ) *SaveNoteHandler {
return &SaveNoteHandler{uow, nr, ts, ns, ls} return &SaveNoteHandler{uow, nr, ts, ns, ls, cfs}
} }
func (h *SaveNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any, error) { func (h *SaveNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any, error) {
@ -84,6 +86,10 @@ func (h *SaveNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any,
return nil, err return nil, err
} }
if err = h.cfeSvc.AddCfe(ctx, tx, note.ID(), file.FrontMatter.Custom); err != nil {
return nil, err
}
if err := h.uow.Commit(); err != nil { if err := h.uow.Commit(); err != nil {
return nil, err return nil, err
} }

View file

@ -3,6 +3,7 @@ package vault
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"log/slog" "log/slog"
"github.com/KristianJBorgwarth/dendrite.daemon/core/services" "github.com/KristianJBorgwarth/dendrite.daemon/core/services"
@ -11,11 +12,22 @@ import (
) )
type initializeCommand struct { type initializeCommand struct {
VaultName string `json:"vaultName"` Config Config `json:"config"`
VaultPath string `json:"vaultPath"` }
TemplateDirectory string `json:"templateDirectory"`
ExcludeIndexFiles []string `json:"excludeIndexFiles"` type Config struct {
OverrideDefaultIgnores bool `json:"overrideDefaultIgnores"` VaultName string `json:"vault_name"`
VaultPath string `json:"vault_path"`
TemplatesDir string `json:"templates_dir"`
ExcludeIndexFiles []string `json:"exclude_index_files"`
OverrideDefaultIgnores bool `json:"override_default_ignores"`
DailyNotes DailyNotes `json:"daily_notes"`
}
type DailyNotes struct {
Dir string `json:"dir"`
FilenameFormat string `json:"filename_format"`
TemplateName string `json:"template_name"`
} }
type InitializeHandler struct { type InitializeHandler struct {
@ -34,12 +46,20 @@ func (h InitializeHandler) Handle(ctx context.Context, raw json.RawMessage) (any
return nil, err return nil, err
} }
if cmd.Config.VaultPath == "" {
return nil, errors.New("vault_path is empty — check your dendrite.nvim config")
}
store := store.NewVaultStore( store := store.NewVaultStore(
cmd.VaultName, cmd.Config.VaultName,
cmd.VaultPath, cmd.Config.VaultPath,
cmd.TemplateDirectory, cmd.Config.TemplatesDir,
cmd.ExcludeIndexFiles, cmd.Config.ExcludeIndexFiles,
cmd.OverrideDefaultIgnores) cmd.Config.OverrideDefaultIgnores,
cmd.Config.DailyNotes.Dir,
cmd.Config.DailyNotes.FilenameFormat,
cmd.Config.DailyNotes.TemplateName,
)
err := persistence.InitializeDBContext(store.Config.VaultName()) err := persistence.InitializeDBContext(store.Config.VaultName())
if err != nil { if err != nil {
@ -56,7 +76,7 @@ func (h InitializeHandler) Handle(ctx context.Context, raw json.RawMessage) (any
return nil, nil return nil, nil
} }
err = h.idxRebuilder.RebuildIndex(ctx, cmd.VaultPath) err = h.idxRebuilder.RebuildIndex(ctx, cmd.Config.VaultPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -0,0 +1,27 @@
package models
type CustomFronMatter struct {
nodeID string
key string
value string
}
func NewCustomFrontMatter(nodeID, key, value string) *CustomFronMatter {
return &CustomFronMatter{
nodeID: nodeID,
key: key,
value: value,
}
}
func (cfm *CustomFronMatter) NodeID() string {
return cfm.nodeID
}
func (cfm *CustomFronMatter) Key() string {
return cfm.key
}
func (cfm *CustomFronMatter) Value() string {
return cfm.value
}

View file

@ -1,6 +1,11 @@
package models package models
import filehandling "github.com/KristianJBorgwarth/dendrite.daemon/core/file_handling" import (
"errors"
"log/slog"
filehandling "github.com/KristianJBorgwarth/dendrite.daemon/core/file_handling"
)
func MapToLinkModel(noteID string, extractedLinks []*filehandling.ExtractedLink) []*Link { func MapToLinkModel(noteID string, extractedLinks []*filehandling.ExtractedLink) []*Link {
var links []*Link var links []*Link
@ -9,3 +14,24 @@ func MapToLinkModel(noteID string, extractedLinks []*filehandling.ExtractedLink)
} }
return links return links
} }
func MapToCustomFrontmatter(noteID string, extractedCfe map[string]any) ([]*CustomFronMatter, error) {
var cfe []*CustomFronMatter
for key, value := range extractedCfe {
if valueStr, ok := value.(string); ok {
cfe = append(cfe, NewCustomFrontMatter(noteID, key, valueStr))
continue
} else if valueArr, ok := value.([]any); ok {
for _, v := range valueArr {
if s, ok := v.(string); ok {
cfe = append(cfe, NewCustomFrontMatter(noteID, key, s))
}
}
continue
} else {
slog.Warn("Unsupported CFE value type, skipping", "key", key, "value", value)
return nil, errors.New("unsupported CFE value type")
}
}
return cfe, nil
}

View file

@ -0,0 +1,37 @@
package services
import (
"context"
"github.com/KristianJBorgwarth/dendrite.daemon/core/models"
"github.com/KristianJBorgwarth/dendrite.daemon/persistence"
"github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories"
)
type ICfService interface {
AddCfe(ctx context.Context, dbCtx persistence.IDbContext, noteID string, cfe map[string]any) error
}
type cfService struct {
cfRepo repositories.ICfRepository
}
func NewCfeService(cfRepo repositories.ICfRepository) ICfService {
return &cfService{cfRepo}
}
func (s *cfService) AddCfe(
ctx context.Context,
dbCtx persistence.IDbContext,
noteID string,
cfe map[string]any,
) error {
if len(cfe) == 0 {
return nil
}
cfModels, err := models.MapToCustomFrontmatter(noteID, cfe)
if err != nil {
return err
}
return s.cfRepo.InsertRange(ctx, dbCtx, cfModels)
}

View file

@ -15,6 +15,14 @@ import (
"github.com/KristianJBorgwarth/dendrite.daemon/persistence/store" "github.com/KristianJBorgwarth/dendrite.daemon/persistence/store"
) )
type index struct {
notes []*models.Note
links []*models.Link
cfe []*models.CustomFronMatter
tags []*models.Tag
noteTags []*models.NoteTag
}
type IIndexRebuilder interface { type IIndexRebuilder interface {
RebuildIndex(ctx context.Context, vaultRoot string) error RebuildIndex(ctx context.Context, vaultRoot string) error
} }
@ -25,6 +33,7 @@ type indexRebuilder struct {
linkRepo repositories.ILinkRepository linkRepo repositories.ILinkRepository
tagRepo repositories.ITagRepository tagRepo repositories.ITagRepository
indexRepo repositories.IIndexRepository indexRepo repositories.IIndexRepository
cfeRepo repositories.ICfRepository
} }
func NewIndexRebuilder( func NewIndexRebuilder(
@ -33,6 +42,7 @@ func NewIndexRebuilder(
linkRepo repositories.ILinkRepository, linkRepo repositories.ILinkRepository,
tagRepo repositories.ITagRepository, tagRepo repositories.ITagRepository,
indexRepo repositories.IIndexRepository, indexRepo repositories.IIndexRepository,
cfeRepo repositories.ICfRepository,
) *indexRebuilder { ) *indexRebuilder {
return &indexRebuilder{ return &indexRebuilder{
uow: uow, uow: uow,
@ -40,6 +50,7 @@ func NewIndexRebuilder(
linkRepo: linkRepo, linkRepo: linkRepo,
tagRepo: tagRepo, tagRepo: tagRepo,
indexRepo: indexRepo, indexRepo: indexRepo,
cfeRepo: cfeRepo,
} }
} }
@ -54,13 +65,16 @@ func (r *indexRebuilder) RebuildIndex(ctx context.Context, vaultRoot string) err
return err return err
} }
notes, links, tags, noteTags := r.buildDBModels(files) index, err := r.buildDBModels(files)
if err != nil {
return err
}
if err = r.indexRepo.WipeIndex(ctx, dbctx); err != nil { if err = r.indexRepo.WipeIndex(ctx, dbctx); err != nil {
return err return err
} }
if err = r.buildIndex(ctx, dbctx, notes, links, tags, noteTags); err != nil { if err = r.buildIndex(ctx, dbctx, index); err != nil {
return err return err
} }
@ -74,24 +88,25 @@ func (r *indexRebuilder) RebuildIndex(ctx context.Context, vaultRoot string) err
func (r *indexRebuilder) buildIndex( func (r *indexRebuilder) buildIndex(
ctx context.Context, ctx context.Context,
dbctx persistence.IDbContext, dbctx persistence.IDbContext,
notes []*models.Note, index *index,
links []*models.Link,
tags []*models.Tag,
noteTags []*models.NoteTag,
) error { ) error {
if err := r.noteRepo.InsertRange(ctx, dbctx, notes); err != nil { if err := r.noteRepo.InsertRange(ctx, dbctx, index.notes); err != nil {
return err return err
} }
if err := r.linkRepo.InsertRange(ctx, dbctx, links); err != nil { if err := r.linkRepo.InsertRange(ctx, dbctx, index.links); err != nil {
return err return err
} }
if err := r.tagRepo.InsertRange(ctx, dbctx, tags); err != nil { if err := r.tagRepo.InsertRange(ctx, dbctx, index.tags); err != nil {
return err return err
} }
if err := r.tagRepo.InsertNoteTags(ctx, dbctx, noteTags); err != nil { if err := r.tagRepo.InsertNoteTags(ctx, dbctx, index.noteTags); err != nil {
return err
}
if err := r.cfeRepo.InsertRange(ctx, dbctx, index.cfe); err != nil {
return err return err
} }
@ -146,9 +161,10 @@ func (r *indexRebuilder) IsValidDirectory(path string) bool {
return true return true
} }
func (r *indexRebuilder) buildDBModels(files []*filehandling.File) ([]*models.Note, []*models.Link, []*models.Tag, []*models.NoteTag) { func (r *indexRebuilder) buildDBModels(files []*filehandling.File) (*index, error) {
var notes []*models.Note var notes []*models.Note
var links []*models.Link var links []*models.Link
var cfe []*models.CustomFronMatter
tagMap := make(map[string]*models.Tag) tagMap := make(map[string]*models.Tag)
var noteTags []*models.NoteTag var noteTags []*models.NoteTag
@ -160,6 +176,12 @@ func (r *indexRebuilder) buildDBModels(files []*filehandling.File) ([]*models.No
} }
noteTags = append(noteTags, models.CreateNoteTags(note.ID(), file.FrontMatter.Tags)...) noteTags = append(noteTags, models.CreateNoteTags(note.ID(), file.FrontMatter.Tags)...)
links = append(links, models.MapToLinkModel(note.ID(), file.ExtractedLinks)...) links = append(links, models.MapToLinkModel(note.ID(), file.ExtractedLinks)...)
mappedCfe, err := models.MapToCustomFrontmatter(note.ID(), file.FrontMatter.Custom)
if err != nil {
return nil, err
}
cfe = append(cfe, mappedCfe...)
} }
tags := make([]*models.Tag, 0, len(tagMap)) tags := make([]*models.Tag, 0, len(tagMap))
@ -167,5 +189,11 @@ func (r *indexRebuilder) buildDBModels(files []*filehandling.File) ([]*models.No
tags = append(tags, tag) tags = append(tags, tag)
} }
return notes, links, tags, noteTags return &index{
notes: notes,
links: links,
tags: tags,
noteTags: noteTags,
cfe: cfe,
}, nil
} }

View file

@ -21,14 +21,16 @@ type noteService struct {
tagRepo repositories.ITagRepository tagRepo repositories.ITagRepository
linkRepo repositories.ILinkRepository linkRepo repositories.ILinkRepository
noteRepo repositories.INoteRepository noteRepo repositories.INoteRepository
cfeRepo repositories.ICfRepository
} }
func NewNoteService( func NewNoteService(
tagRepo repositories.ITagRepository, tagRepo repositories.ITagRepository,
linkRepo repositories.ILinkRepository, linkRepo repositories.ILinkRepository,
noteRepo repositories.INoteRepository, noteRepo repositories.INoteRepository,
cfeRepo repositories.ICfRepository,
) INoteService { ) INoteService {
return &noteService{tagRepo, linkRepo, noteRepo} return &noteService{tagRepo, linkRepo, noteRepo, cfeRepo}
} }
func (s *noteService) CreateNote(ctx context.Context, dbCtx persistence.IDbContext, path, title, slug string) (*models.Note, error) { func (s *noteService) CreateNote(ctx context.Context, dbCtx persistence.IDbContext, path, title, slug string) (*models.Note, error) {
@ -56,6 +58,10 @@ func (s *noteService) DeleteNoteMetaData(ctx context.Context, dbCtx persistence.
return err return err
} }
if err := s.cfeRepo.Delete(ctx, dbCtx, noteID); err != nil {
return err
}
return nil return nil
} }

BIN
dendrite

Binary file not shown.

View file

@ -49,8 +49,10 @@ ASSET="${BINARY}-${GOOS}-${GOARCH}"
URL="https://github.com/$REPO/releases/download/$TAG/$ASSET" URL="https://github.com/$REPO/releases/download/$TAG/$ASSET"
echo "Downloading $ASSET ($TAG)..." echo "Downloading $ASSET ($TAG)..."
curl -fsSL "$URL" -o "$INSTALL_DIR/$BINARY" TMP="$(mktemp)"
chmod +x "$INSTALL_DIR/$BINARY" curl -fsSL "$URL" -o "$TMP"
chmod +x "$TMP"
mv "$TMP" "$INSTALL_DIR/$BINARY"
echo "" echo ""
echo "dendrite $TAG installed to $INSTALL_DIR/$BINARY" echo "dendrite $TAG installed to $INSTALL_DIR/$BINARY"

View file

@ -7,3 +7,6 @@ test-unit:
test-integration: test-integration:
go test ./test/test_integration/... go test ./test/test_integration/...
build:
go build -o dendrite ./cmd

View file

@ -1,50 +1,61 @@
package persistenceconfig package persistenceconfig
type VaultConfiguration struct { type DailyNotes struct {
name string dir string
path string filenameFormat string
templateDirectory string templateName string
exludeIndexFiles []string }
defaultExcludes []string
func NewDailyNotes(dir, filenameFormat, templateName string) DailyNotes {
return DailyNotes{
dir: dir,
filenameFormat: filenameFormat,
templateName: templateName,
}
}
type VaultConfig struct {
vaultName string
vaultPath string
templatesDir string
excludeIndexFiles []string
overrideDefaultIgnores bool overrideDefaultIgnores bool
dailyNotes DailyNotes
} }
func NewVaultConfiguration( func NewVaultConfiguration(
vaultName, vaultName,
vaultPath, vaultPath,
templateDirectory string, templatesDir string,
excludeIndexFiles []string, excludeIndexFiles []string,
overrideDefaultIgnores bool, overrideDefaultIgnores bool,
) *VaultConfiguration { dailyNotes DailyNotes,
return &VaultConfiguration{ ) *VaultConfig {
name: vaultName, return &VaultConfig{
path: vaultPath, vaultName: vaultName,
templateDirectory: templateDirectory, vaultPath: vaultPath,
exludeIndexFiles: excludeIndexFiles, templatesDir: templatesDir,
defaultExcludes: []string{".git", ".templates"}, dailyNotes: dailyNotes,
} }
} }
func (vc *VaultConfiguration) VaultName() string { func (vc *VaultConfig) VaultName() string {
return vc.name return vc.vaultName
} }
func (vc *VaultConfiguration) VaultPath() string { func (vc *VaultConfig) VaultPath() string {
return vc.path return vc.vaultPath
} }
func (vc *VaultConfiguration) TemplateDirectory() string { func (vc *VaultConfig) TemplateDirectory() string {
return vc.templateDirectory return vc.templatesDir
} }
func (vc *VaultConfiguration) ExcludeIndexFiles() []string { func (vc *VaultConfig) ExcludeIndexFiles() []string {
return vc.exludeIndexFiles defaultIgnores := []string{"index.md", "index", ".templates"}
}
func (vc *VaultConfiguration) GetExcludeIndexFiles() []string {
defaultIgnores := []string{".git", ".templates"}
if vc.overrideDefaultIgnores { if vc.overrideDefaultIgnores {
return vc.exludeIndexFiles return vc.excludeIndexFiles
} }
return append(defaultIgnores, vc.exludeIndexFiles...) return append(defaultIgnores, vc.excludeIndexFiles...)
} }

View file

@ -0,0 +1,9 @@
CREATE TABLE custom_frontmatter(
note_id TEXT,
key TEXT,
value TEXT,
PRIMARY KEY (note_id, key, value),
FOREIGN KEY (note_id) REFERENCES note(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_cfe_key_value on custom_frontmatter(key, value);

View file

@ -0,0 +1,91 @@
package repositories
import (
"context"
"github.com/KristianJBorgwarth/dendrite.daemon/core/models"
"github.com/KristianJBorgwarth/dendrite.daemon/persistence"
)
type ICfRepository 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
}
type cfeRepository struct {
readDBContext persistence.ReadContext
}
func NewCfeRepository(rdb persistence.ReadContext) ICfRepository {
return &cfeRepository{readDBContext: rdb}
}
func (r *cfeRepository) InsertRange(
ctx context.Context,
dbCtx persistence.IDbContext,
cfe []*models.CustomFronMatter,
) error {
statement, err := dbCtx.Prepare(
`INSERT INTO custom_frontmatter (note_id, key, value)
VALUES (?, ?, ?) ON CONFLICT DO NOTHING;`)
if err != nil {
return err
}
for _, c := range cfe {
if _, err := statement.ExecContext(ctx, c.NodeID(), c.Key(), c.Value()); err != nil {
return err
}
}
return nil
}
func (r *cfeRepository) Delete(
ctx context.Context,
dbCtx persistence.IDbContext,
noteID string,
) error {
statement, err := dbCtx.Prepare(`DELETE FROM custom_frontmatter WHERE note_id = ?;`)
if err != nil {
return err
}
if _, err := statement.ExecContext(ctx, noteID); err != nil {
return err
}
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
}

View file

@ -22,7 +22,8 @@ func (r *indexRepository) WipeIndex(ctx context.Context, dbContext persistence.I
cmd := `DELETE FROM note; cmd := `DELETE FROM note;
DELETE FROM tag; DELETE FROM tag;
DELETE FROM note_tag; DELETE FROM note_tag;
DELETE FROM link;` DELETE FROM link;
DELETE FROM custom_frontmatter;`
_, err := dbContext.ExecContext(ctx, cmd) _, err := dbContext.ExecContext(ctx, cmd)
return err return err

View file

@ -7,7 +7,7 @@ import (
) )
type VaultStore struct { type VaultStore struct {
Config *persistenceconfig.VaultConfiguration Config *persistenceconfig.VaultConfig
} }
var vaultStore *VaultStore var vaultStore *VaultStore
@ -18,6 +18,9 @@ func NewVaultStore(
templateDir string, templateDir string,
exludeIndexFiles []string, exludeIndexFiles []string,
overrideDefaultIgnores bool, overrideDefaultIgnores bool,
dailyDir string,
dailyFilenameFormat string,
dailyTemplateName string,
) *VaultStore { ) *VaultStore {
if vaultStore != nil { if vaultStore != nil {
return vaultStore return vaultStore
@ -28,7 +31,7 @@ func NewVaultStore(
templateDir, templateDir,
exludeIndexFiles, exludeIndexFiles,
overrideDefaultIgnores, overrideDefaultIgnores,
)} persistenceconfig.NewDailyNotes(dailyDir, dailyFilenameFormat, dailyTemplateName))}
return vaultStore return vaultStore
} }
@ -39,13 +42,10 @@ func GetVaultStore() *VaultStore {
return vaultStore return vaultStore
} }
func (vs *VaultStore) SetConfig(config persistenceconfig.VaultConfiguration) {
vs.Config = &config
}
func (vs *VaultStore) GetTemplatePath(templateName string) string { func (vs *VaultStore) GetTemplatePath(templateName string) string {
templateName = vs.fileTypeCheck(templateName) templateName = vs.fileTypeCheck(templateName)
return path.Join(vs.Config.TemplateDirectory(), templateName) path := path.Join(vs.Config.VaultPath(), vs.Config.TemplateDirectory(), templateName)
return path
} }
func (vs *VaultStore) fileTypeCheck(templateName string) string { func (vs *VaultStore) fileTypeCheck(templateName string) string {
@ -57,5 +57,5 @@ func (vs *VaultStore) fileTypeCheck(templateName string) string {
} }
func (vs *VaultStore) GetExcludeIndexFiles() []string { func (vs *VaultStore) GetExcludeIndexFiles() []string {
return vs.Config.GetExcludeIndexFiles() return vs.Config.ExcludeIndexFiles()
} }

View file

@ -19,6 +19,7 @@ func newCreateNoteHandler() *note.CreateNoteHandler {
repositories.NewUnitOfWork(), repositories.NewUnitOfWork(),
services.NewTagService(repositories.NewTagRepository(persistence.NewReadContext())), services.NewTagService(repositories.NewTagRepository(persistence.NewReadContext())),
repositories.NewNoteRepository(persistence.NewReadContext()), repositories.NewNoteRepository(persistence.NewReadContext()),
services.NewCfeService(repositories.NewCfeRepository(persistence.NewReadContext())),
) )
} }
@ -54,7 +55,7 @@ func TestCreateNoteHandler_WithTemplate_CreatesNoteFileWithTagsAndReturnsPath(t
handler := newCreateNoteHandler() handler := newCreateNoteHandler()
vaultPath := Fixture.VaultStore.Config.VaultPath() vaultPath := Fixture.VaultStore.Config.VaultPath()
templateDir := Fixture.VaultStore.Config.TemplateDirectory() templateDir := filepath.Join(Fixture.VaultStore.Config.VaultPath(), Fixture.VaultStore.Config.TemplateDirectory())
require.NoError(t, os.MkdirAll(templateDir, 0o755)) require.NoError(t, os.MkdirAll(templateDir, 0o755))
templatePath := filepath.Join(templateDir, "template.md") templatePath := filepath.Join(templateDir, "template.md")

View file

@ -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.GetNotesByCfHandler {
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)
}

View file

@ -5,7 +5,6 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"os" "os"
"path"
"path/filepath" "path/filepath"
"testing" "testing"
@ -46,7 +45,7 @@ func NewDBFixture() *DBFixture {
DB: dbContext.DB, DB: dbContext.DB,
DBPath: dbPath, DBPath: dbPath,
TestContext: context.Background(), TestContext: context.Background(),
VaultStore: store.NewVaultStore("testVault", vaultPath, path.Join(vaultPath, "templates"), []string{}, false), VaultStore: store.NewVaultStore("testVault", vaultPath, "templates", []string{}, false, "", "", ""),
} }
} }
@ -54,9 +53,7 @@ var Fixture = NewDBFixture()
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
code := m.Run() code := m.Run()
os.RemoveAll(Fixture.DBPath) os.RemoveAll(Fixture.DBPath)
os.Exit(code) os.Exit(code)
} }

View file

@ -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
}

View file

@ -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
}

View file

@ -0,0 +1,2 @@
// Package utils provides testing utilities for integration tests
package utils