Merge pull request #15 from KristianJBorgwarth/ref/link-model

feat(link_model): updated link model and added link repo
This commit is contained in:
Kristian 2026-04-08 22:19:40 +02:00 committed by GitHub
commit 25098f0778
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 131 additions and 39 deletions

View file

@ -1,16 +1,42 @@
package models package models
import "github.com/google/uuid"
type Link struct { type Link struct {
id string
fromNoteID string fromNoteID string
toNoteID string targetSlug string
raw string raw string
display string
line int
col int
} }
func NewLink(fromNoteID, toNoteID, raw string) *Link { func NewLink(id, fromNoteID, targetSlug, raw, display string, line, col int) *Link {
return &Link{ return &Link{
id: id,
fromNoteID: fromNoteID, fromNoteID: fromNoteID,
toNoteID: toNoteID, targetSlug: targetSlug,
raw: raw, raw: raw,
display: display,
line: line,
col: col,
}
}
func CreateLink(fromNoteID, targetSlug, raw, display string, line, col int) *Link {
id, err := uuid.NewV7()
if err != nil {
panic(err)
}
return &Link{
id: id.String(),
fromNoteID: fromNoteID,
targetSlug: targetSlug,
raw: raw,
display: display,
line: line,
col: col,
} }
} }
@ -18,8 +44,8 @@ func (l *Link) FromNoteID() string {
return l.fromNoteID return l.fromNoteID
} }
func (l *Link) ToNoteID() string { func (l *Link) TargetSlug() string {
return l.toNoteID return l.targetSlug
} }
func (l *Link) Raw() string { func (l *Link) Raw() string {

View file

@ -1,21 +0,0 @@
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
path TEXT UNIQUE,
title TEXT,
slug TEXT UNIQUE NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS links (
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
);
CREATE INDEX IF NOT EXISTS idx_notes_slug ON notes(slug);
CREATE INDEX IF NOT EXISTS idx_links_from ON links(from_note_id);
CREATE INDEX IF NOT EXISTS idx_links_to ON links(to_note_id);

View file

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS note (
id TEXT PRIMARY KEY,
path TEXT UNIQUE,
title TEXT,
slug TEXT UNIQUE NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_note_slug ON note(slug);

View file

@ -1,12 +1,12 @@
CREATE TABLE IF NOT EXISTS tags ( CREATE TABLE IF NOT EXISTS tag (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT UNIQUE name TEXT UNIQUE
); );
CREATE TABLE IF NOT EXISTS note_tags ( CREATE TABLE IF NOT EXISTS note_tag (
note_id TEXT, note_id TEXT,
tag_id TEXT, tag_id TEXT,
PRIMARY KEY (note_id, tag_id), PRIMARY KEY (note_id, tag_id),
FOREIGN KEY(note_id) REFERENCES notes(id) ON DELETE CASCADE, FOREIGN KEY(note_id) REFERENCES note(id) ON DELETE CASCADE,
FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE FOREIGN KEY(tag_id) REFERENCES tag(id) ON DELETE CASCADE
); );

View file

@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS link (
id TEXT PRIMARY KEY,
from_note_id TEXT NOT NULL,
target_slug TEXT NOT NULL,
display TEXT,
raw TEXT NOT NULL,
line INTEGER NOT NULL,
col INTEGER NOT NULL,
FOREIGN KEY(from_note_id) REFERENCES note(id) ON DELETE CASCADE
);
CREATE INDEX idx_link_from ON link(from_note_id);
CREATE INDEX idx_link_target ON link(target_slug);

View file

@ -0,0 +1,61 @@
package repositories
import (
"context"
"database/sql"
"github.com/KristianJBorgwarth/dendrite.daemon/core/models"
)
type ILinkRepository interface {
GetByNoteID(ctx context.Context, fromNoteID string) ([]*models.Link, error)
GetBySlug(ctx context.Context, targetSlug string) ([]*models.Link, error)
}
type linkRepository struct {
Transaction *sql.Tx
}
func NewLinkRepository(tx *sql.Tx) ILinkRepository {
return &linkRepository{Transaction: tx}
}
func (r *linkRepository) GetByNoteID(ctx context.Context, fromNoteID string) ([]*models.Link, error) {
rows, err := r.Transaction.QueryContext(ctx, "SELECT id, from_note_id, target_slug, raw, display, line, col FROM links WHERE from_note_id = ?", fromNoteID)
if err != nil {
return nil, err
}
defer rows.Close()
var links []*models.Link
for rows.Next() {
var id, fromNoteID, targetSlug, raw, display string
var line, col int
if err := rows.Scan(&id, &fromNoteID, &targetSlug, &raw, &display, &line, &col); err != nil {
return nil, err
}
links = append(links, models.NewLink(id, fromNoteID, targetSlug, raw, display, line, col))
}
return links, nil
}
func (r *linkRepository) GetBySlug(ctx context.Context, targetSlug string) ([]*models.Link, error) {
rows, err := r.Transaction.QueryContext(ctx, `SELECT id, from_note_id, target_slug, raw, display, line, col FROM links WHERE target_slug = ?`, targetSlug)
if err != nil {
return nil, err
}
defer rows.Close()
var links []*models.Link
for rows.Next() {
var id, fromNoteID, targetSlug, raw, display string
var line, col int
if err := rows.Scan(&id, &fromNoteID, &targetSlug, &raw, &display, &line, &col); err != nil {
return nil, err
}
links = append(links, models.NewLink(id, fromNoteID, targetSlug, raw, display, line, col))
}
return links, nil
}

View file

@ -21,7 +21,7 @@ func NewNoteRepository(tx *sql.Tx) NoteRepository {
func (r *noteRepository) Upsert(ctx context.Context, note *models.Note) error { func (r *noteRepository) Upsert(ctx context.Context, note *models.Note) error {
query := ` query := `
INSERT INTO notes (id, title, path, slug, created_at, updated_at) INSERT INTO note (id, title, path, slug, created_at, updated_at)
VALUES (?, ?, ?, ?, datetime('now'), datetime('now')) VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))
ON CONFLICT (slug) DO UPDATE ON CONFLICT (slug) DO UPDATE
SET title = EXCLUDED.title, SET title = EXCLUDED.title,

View file

@ -35,7 +35,7 @@ func (r *tagRepository) Upsert(ctx context.Context, tags []*models.Tag) error {
args = append(args, tag.ID(), tag.Name()) args = append(args, tag.ID(), tag.Name())
} }
query := "INSERT OR IGNORE INTO tags(id, name) VALUES " + strings.Join(placeholders, ",") query := "INSERT OR IGNORE INTO tag(id, name) VALUES " + strings.Join(placeholders, ",")
_, err := r.Transaction.ExecContext(ctx, query, args...) _, err := r.Transaction.ExecContext(ctx, query, args...)
return err return err
@ -54,7 +54,7 @@ func (r *tagRepository) UpsertNoteTags(ctx context.Context ,noteID string, tagID
args = append(args, noteID, tagID) args = append(args, noteID, tagID)
} }
query := "INSERT OR IGNORE INTO note_tags(note_id, tag_id) VALUES " + strings.Join(placeholders, ",") query := "INSERT OR IGNORE INTO note_tag(note_id, tag_id) VALUES " + strings.Join(placeholders, ",")
_, err := r.Transaction.ExecContext(ctx, query, args...) _, err := r.Transaction.ExecContext(ctx, query, args...)
if err != nil { if err != nil {
@ -77,7 +77,7 @@ func (r *tagRepository) GetByNames(ctx context.Context, names []string) ([]*mode
args = append(args, name) args = append(args, name)
} }
query := "SELECT id, name FROM tags WHERE name IN (" + strings.Join(placeholders, ",") + ")" query := "SELECT id, name FROM tag WHERE name IN (" + strings.Join(placeholders, ",") + ")"
rows, err := r.Transaction.QueryContext(ctx, query, args...) rows, err := r.Transaction.QueryContext(ctx, query, args...)
if err != nil { if err != nil {

View file

@ -31,7 +31,7 @@ func TestCreateNoteHandler_NoTemplate_CreatesNoteFileAndReturnsPath(t *testing.T
assert.Equal(t, notePath, result) assert.Equal(t, notePath, result)
var count int var count int
require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM notes WHERE slug = ?`, "my-note").Scan(&count)) require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM note WHERE slug = ?`, "my-note").Scan(&count))
assert.Equal(t, 1, count) assert.Equal(t, 1, count)
_, statErr := os.Stat(notePath) _, statErr := os.Stat(notePath)
@ -68,10 +68,10 @@ func TestCreateNoteHandler_WithTemplate_CreatesNoteFileWithTagsAndReturnsPath(t
assert.Equal(t, notePath, result) assert.Equal(t, notePath, result)
var noteCount int var noteCount int
require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM notes WHERE slug = ?`, "templated-note").Scan(&noteCount)) require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM note WHERE slug = ?`, "templated-note").Scan(&noteCount))
assert.Equal(t, 1, noteCount) assert.Equal(t, 1, noteCount)
rows, err := Fixture.DB.Query(`SELECT name FROM tags WHERE name IN ('go', 'testing')`) rows, err := Fixture.DB.Query(`SELECT name FROM tag WHERE name IN ('go', 'testing')`)
require.NoError(t, err) require.NoError(t, err)
defer rows.Close() defer rows.Close()
@ -112,12 +112,12 @@ func TestCreateNoteHandler_DuplicateSlug_Upserts(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
var count int var count int
require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM notes WHERE slug = ?`, "dup-note").Scan(&count)) require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM note WHERE slug = ?`, "dup-note").Scan(&count))
assert.Equal(t, 1, count) assert.Equal(t, 1, count)
expectedPath := filepath.Join(vaultPath, subDir2, "dup-note.md") expectedPath := filepath.Join(vaultPath, subDir2, "dup-note.md")
var path string var path string
require.NoError(t, Fixture.DB.QueryRow(`SELECT path FROM notes WHERE slug = ?`, "dup-note").Scan(&path)) require.NoError(t, Fixture.DB.QueryRow(`SELECT path FROM note WHERE slug = ?`, "dup-note").Scan(&path))
assert.Equal(t, expectedPath, path) assert.Equal(t, expectedPath, path)
} }