diff --git a/core/handlers/create_note_handler.go b/core/handlers/create_note_handler.go index 6a045f6..a61c7fe 100644 --- a/core/handlers/create_note_handler.go +++ b/core/handlers/create_note_handler.go @@ -5,7 +5,9 @@ import ( "encoding/json" "github.com/KristianJBorgwarth/dendrite.daemon/core/frontmatter" + "github.com/KristianJBorgwarth/dendrite.daemon/core/models" "github.com/KristianJBorgwarth/dendrite.daemon/core/template" + "github.com/KristianJBorgwarth/dendrite.daemon/core/utils" "github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories" ) @@ -26,6 +28,7 @@ func NewCreateNoteHandler(uow *repositories.UnitOfWork) *CreateNoteHandler { func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any, error) { var cmd createNoteCommand + if err := json.Unmarshal(raw, &cmd); err != nil { return nil, err } @@ -46,16 +49,28 @@ func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (an if err != nil { return nil, err } + defer h.uow.Rollback() tagRepo := repositories.NewTagRepository(tx) noteRepo := repositories.NewNoteRepository(tx) - if err = tagRepo.Upsert(ctx, tags); err != nil { + tagModels, err := models.CreateTags(tags) + if err != nil { return nil, err } - if err = noteRepo.Upsert(ctx, cmd.Title, cmd.Path, slug); err != nil { + if err = tagRepo.Upsert(ctx, tagModels); err != nil { + return nil, err + } + + note := models.CreateNote(cmd.Path, cmd.Title, slug) + + if err = noteRepo.Upsert(ctx, note); err != nil { + return nil, err + } + + if err = tagRepo.UpsertNoteTags(note.ID(), utils.Select(tagModels, func(t *models.Tag) string { return t.ID() })); err != nil { return nil, err } diff --git a/core/models/links.go b/core/models/links.go index 538a2d8..c870840 100644 --- a/core/models/links.go +++ b/core/models/links.go @@ -1,12 +1,12 @@ package models type Link struct { - fromNoteID int - toNoteID int + fromNoteID string + toNoteID string raw string } -func NewLink(fromNoteID, toNoteID int, raw string) *Link { +func NewLink(fromNoteID, toNoteID, raw string) *Link { return &Link{ fromNoteID: fromNoteID, toNoteID: toNoteID, @@ -14,11 +14,11 @@ func NewLink(fromNoteID, toNoteID int, raw string) *Link { } } -func (l *Link) FromNoteID() int { +func (l *Link) FromNoteID() string { return l.fromNoteID } -func (l *Link) ToNoteID() int { +func (l *Link) ToNoteID() string { return l.toNoteID } diff --git a/core/models/note.go b/core/models/note.go index c95b71b..8894198 100644 --- a/core/models/note.go +++ b/core/models/note.go @@ -1,7 +1,13 @@ package models +import ( + "time" + + "github.com/google/uuid" +) + type Note struct { - id int + id string path string title string slug string @@ -9,7 +15,7 @@ type Note struct { updatedAt string } -func NewNote(id int, path, title, slug, createdAt, updatedAt string) *Note { +func NewNote(id, path, title, slug, createdAt, updatedAt string) *Note { return &Note{ id: id, path: path, @@ -20,7 +26,22 @@ func NewNote(id int, path, title, slug, createdAt, updatedAt string) *Note { } } -func (n *Note) ID() int { +func CreateNote(path, title, slug string) *Note { + id, err := uuid.NewV7() + if err != nil { + panic(err) + } + return &Note{ + id: id.String(), + path: path, + title: title, + slug: slug, + createdAt: time.Now().Format("2006-01-02 15:04:05"), + updatedAt: time.Now().Format("2006-01-02 15:04:05"), + } +} + +func (n *Note) ID() string { return n.id } diff --git a/core/models/note_tag.go b/core/models/note_tag.go index ec36634..c7e6090 100644 --- a/core/models/note_tag.go +++ b/core/models/note_tag.go @@ -1,21 +1,29 @@ package models type NoteTag struct { - noteID int - tagID int + noteID string + tagID string } -func NewNoteTag(noteID, tagID int) *NoteTag { +func NewNoteTag(noteID, tagID string) *NoteTag { return &NoteTag{ noteID: noteID, tagID: tagID, } } -func (nt *NoteTag) NoteID() int { +func NewNoteTags(noteID string, tagIDs []string) []*NoteTag { + noteTags := make([]*NoteTag, len(tagIDs)) + for i, tagID := range tagIDs { + noteTags[i] = NewNoteTag(noteID, tagID) + } + return noteTags +} + +func (nt *NoteTag) NoteID() string { return nt.noteID } -func (nt *NoteTag) TagID() int { +func (nt *NoteTag) TagID() string { return nt.tagID } diff --git a/core/models/tag.go b/core/models/tag.go index e36d306..8f6461b 100644 --- a/core/models/tag.go +++ b/core/models/tag.go @@ -1,18 +1,33 @@ package models +import "github.com/google/uuid" + type Tag struct { - id int + id string name string } -func NewTag(id int, name string) *Tag { +func NewTag(name string) *Tag { + id, err := uuid.NewV7() + if err != nil { + panic(err) + } return &Tag{ - id: id, + id: id.String(), name: name, } } -func (t *Tag) ID() int { +func CreateTags(tags []string) ([]*Tag, error) { + tagModels := make([]*Tag, len(tags)) + for i, tag := range tags { + tagModels[i] = NewTag(tag) + } + + return tagModels, nil +} + +func (t *Tag) ID() string { return t.id } diff --git a/core/utils/doc.go b/core/utils/doc.go new file mode 100644 index 0000000..f8b0649 --- /dev/null +++ b/core/utils/doc.go @@ -0,0 +1,10 @@ +// Package utils provides BASIC FUCKING utilities +package utils + +func Select[T any, U any](input []T, mapper func(T) U) []U { + output := make([]U, len(input)) + for i, item := range input { + output[i] = mapper(item) + } + return output +} diff --git a/go.mod b/go.mod index 1254b4e..c43cce8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/KristianJBorgwarth/dendrite.daemon go 1.26 require ( + github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.11.1 modernc.org/sqlite v1.47.0 ) @@ -10,7 +11,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/persistence/migrations/001_dendrite_db_init.sql b/persistence/migrations/001_dendrite_db_init.sql index 1ebfc49..90584bc 100644 --- a/persistence/migrations/001_dendrite_db_init.sql +++ b/persistence/migrations/001_dendrite_db_init.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS notes ( - id INTEGER PRIMARY KEY, + id TEXT PRIMARY KEY, path TEXT UNIQUE, title TEXT, slug TEXT UNIQUE NOT NULL, @@ -8,8 +8,8 @@ CREATE TABLE IF NOT EXISTS notes ( ); CREATE TABLE IF NOT EXISTS links ( - from_note_id INTEGER NOT NULL, - to_note_id INTEGER NOT NULL, + from_note_id TEXT NOT NULL, + to_note_id TEXT NOT NULL, raw TEXT, FOREIGN KEY(from_note_id) REFERENCES notes(id) ON DELETE CASCADE, FOREIGN KEY(to_note_id) REFERENCES notes(id) ON DELETE CASCADE diff --git a/persistence/migrations/002_tags.sql b/persistence/migrations/002_tags.sql index 739968a..f8e59dc 100644 --- a/persistence/migrations/002_tags.sql +++ b/persistence/migrations/002_tags.sql @@ -1,11 +1,11 @@ CREATE TABLE IF NOT EXISTS tags ( - id INTEGER PRIMARY KEY, + id TEXT PRIMARY KEY, name TEXT UNIQUE ); CREATE TABLE IF NOT EXISTS note_tags ( - note_id INTEGER, - tag_id INTEGER, + note_id TEXT, + tag_id TEXT, PRIMARY KEY (note_id, tag_id), FOREIGN KEY(note_id) REFERENCES notes(id) ON DELETE CASCADE, FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE diff --git a/persistence/repositories/note_repository.go b/persistence/repositories/note_repository.go index e9d9444..e7d3425 100644 --- a/persistence/repositories/note_repository.go +++ b/persistence/repositories/note_repository.go @@ -3,10 +3,12 @@ package repositories import ( "context" "database/sql" + + "github.com/KristianJBorgwarth/dendrite.daemon/core/models" ) type NoteRepository interface { - Upsert(ctx context.Context, title string, path string, slug string) error + Upsert(ctx context.Context, note *models.Note) error } type noteRepository struct { @@ -17,14 +19,14 @@ func NewNoteRepository(tx *sql.Tx) NoteRepository { return ¬eRepository{Transaction: tx} } -func (r *noteRepository) Upsert(ctx context.Context, title string, path string, slug string) error { +func (r *noteRepository) Upsert(ctx context.Context, note *models.Note) error { query := ` - INSERT INTO notes (title, path, slug, created_at, updated_at) - VALUES (?, ?, ?, datetime('now'), datetime('now')) + INSERT INTO notes (id, title, path, slug, created_at, updated_at) + VALUES (?, ?, ?, ?, datetime('now'), datetime('now')) ON CONFLICT (slug) DO UPDATE SET title = EXCLUDED.title, path = EXCLUDED.path; ` - _, err := r.Transaction.ExecContext(ctx, query, title, path, slug) + _, err := r.Transaction.ExecContext(ctx, query, note.ID(), note.Title(), note.Path(), note.Slug()) return err } diff --git a/persistence/repositories/tag_repository.go b/persistence/repositories/tag_repository.go index a49257e..334e846 100644 --- a/persistence/repositories/tag_repository.go +++ b/persistence/repositories/tag_repository.go @@ -4,40 +4,43 @@ import ( "context" "database/sql" "strings" + + "github.com/KristianJBorgwarth/dendrite.daemon/core/models" ) -type TagRepository interface { - Upsert(ctx context.Context, names []string) error +type ITagRepository interface { + Upsert(ctx context.Context, tags []*models.Tag) error + UpsertNoteTags(noteID string, tagIDs []string) error } type tagRepository struct { Transaction *sql.Tx } -func NewTagRepository(tx *sql.Tx) TagRepository { +func NewTagRepository(tx *sql.Tx) ITagRepository { return &tagRepository{Transaction: tx} } -func (r *tagRepository) Upsert(ctx context.Context, names []string) error { - if len(names) == 0 { +func (r *tagRepository) Upsert(ctx context.Context, tags []*models.Tag) error { + if len(tags) == 0 { return nil } - placeholders := make([]string, 0, len(names)) - args := make([]any, 0, len(names)) + placeholders := make([]string, 0, len(tags)) + args := make([]any, 0, len(tags)) - for _, name := range names { - placeholders = append(placeholders, "(?)") - args = append(args, name) + for _, tag := range tags { + placeholders = append(placeholders, "(?, ?)") + args = append(args, tag.ID(), tag.Name()) } - query := "INSERT OR IGNORE INTO tags(name) VALUES " + strings.Join(placeholders, ",") + query := "INSERT OR IGNORE INTO tags(id, name) VALUES " + strings.Join(placeholders, ",") _, err := r.Transaction.ExecContext(ctx, query, args...) return err } -func (r *tagRepository) UpsertNoteTags(noteID int64, tagIDs []int64) error { +func (r *tagRepository) UpsertNoteTags(noteID string, tagIDs []string) error { if len(tagIDs) == 0 { return nil } @@ -50,12 +53,7 @@ func (r *tagRepository) UpsertNoteTags(noteID int64, tagIDs []int64) error { args = append(args, noteID, tagID) } - query := "WITH input(note_id, tag_id) AS (VALUES " + - strings.Join(placeholders, ",") + - ") INSERT INTO note_tags(note_id, tag_id) " + - "SELECT note_id, tag_id FROM input " + - "ON CONFLICT(note_id, tag_id) DO NOTHING" + - "SELECT note_id, tag_id FROM input;" + query := "INSERT OR IGNORE INTO note_tags(note_id, tag_id) VALUES " + strings.Join(placeholders, ",") _, err := r.Transaction.ExecContext(context.Background(), query, args...) if err != nil {