summaryrefslogtreecommitdiff
path: root/internal/store
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-20 11:17:19 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-20 11:17:19 -1000
commit07ba815e8517ee2d3a5fa531361bbd09bdfcbaa7 (patch)
treeca9d9be0f02d5a724a3646f87d4a9f50203249cc /internal/store
parent6a59098c3096f5ebd3a61ef5268cbd480b0f1519 (diff)
Remove Obsidian integration for public server deployment
Obsidian relied on local filesystem access which is incompatible with public server deployment. This removes all Obsidian-related code including: - API client and interface - Store layer methods (SaveNotes, GetNotes, SearchNotes) - Handler methods and routes - UI tab and templates - Configuration fields - Related tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal/store')
-rw-r--r--internal/store/sqlite.go124
-rw-r--r--internal/store/sqlite_test.go327
2 files changed, 2 insertions, 449 deletions
diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go
index b8d0c97..dac3321 100644
--- a/internal/store/sqlite.go
+++ b/internal/store/sqlite.go
@@ -15,9 +15,8 @@ import (
// Cache key constants
const (
- CacheKeyTodoistTasks = "todoist_tasks"
- CacheKeyTrelloBoards = "trello_boards"
- CacheKeyObsidianNotes = "obsidian_notes"
+ CacheKeyTodoistTasks = "todoist_tasks"
+ CacheKeyTrelloBoards = "trello_boards"
CacheKeyPlanToEatMeals = "plantoeat_meals"
)
@@ -234,125 +233,6 @@ func (s *Store) DeleteTasksByIDs(ids []string) error {
return tx.Commit()
}
-// Notes operations
-
-// SaveNotes saves multiple notes to the database
-func (s *Store) SaveNotes(notes []models.Note) error {
- tx, err := s.db.Begin()
- if err != nil {
- return err
- }
- defer tx.Rollback()
-
- stmt, err := tx.Prepare(`
- INSERT OR REPLACE INTO notes
- (filename, title, content, modified, path, tags, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
- `)
- if err != nil {
- return err
- }
- defer stmt.Close()
-
- for _, note := range notes {
- tagsJSON, _ := json.Marshal(note.Tags)
- _, err := stmt.Exec(
- note.Filename,
- note.Title,
- note.Content,
- note.Modified,
- note.Path,
- string(tagsJSON),
- )
- if err != nil {
- return err
- }
- }
-
- return tx.Commit()
-}
-
-// GetNotes retrieves all notes from the database
-func (s *Store) GetNotes(limit int) ([]models.Note, error) {
- query := `
- SELECT filename, title, content, modified, path, tags
- FROM notes
- ORDER BY modified DESC
- `
- var args []interface{}
- if limit > 0 {
- query += " LIMIT ?"
- args = append(args, limit)
- }
-
- rows, err := s.db.Query(query, args...)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var notes []models.Note
- for rows.Next() {
- var note models.Note
- var tagsJSON string
-
- err := rows.Scan(
- &note.Filename,
- &note.Title,
- &note.Content,
- &note.Modified,
- &note.Path,
- &tagsJSON,
- )
- if err != nil {
- return nil, err
- }
-
- json.Unmarshal([]byte(tagsJSON), &note.Tags)
- notes = append(notes, note)
- }
-
- return notes, rows.Err()
-}
-
-// SearchNotes searches notes by title or content
-func (s *Store) SearchNotes(query string) ([]models.Note, error) {
- searchPattern := "%" + query + "%"
- rows, err := s.db.Query(`
- SELECT filename, title, content, modified, path, tags
- FROM notes
- WHERE title LIKE ? OR content LIKE ?
- ORDER BY modified DESC
- `, searchPattern, searchPattern)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var notes []models.Note
- for rows.Next() {
- var note models.Note
- var tagsJSON string
-
- err := rows.Scan(
- &note.Filename,
- &note.Title,
- &note.Content,
- &note.Modified,
- &note.Path,
- &tagsJSON,
- )
- if err != nil {
- return nil, err
- }
-
- json.Unmarshal([]byte(tagsJSON), &note.Tags)
- notes = append(notes, note)
- }
-
- return notes, rows.Err()
-}
-
// Meals operations
// SaveMeals saves multiple meals to the database
diff --git a/internal/store/sqlite_test.go b/internal/store/sqlite_test.go
index 962a1bf..6bf7783 100644
--- a/internal/store/sqlite_test.go
+++ b/internal/store/sqlite_test.go
@@ -2,7 +2,6 @@ package store
import (
"database/sql"
- "fmt"
"path/filepath"
"testing"
"time"
@@ -11,332 +10,6 @@ import (
"task-dashboard/internal/models"
)
-// setupTestStore creates a test store with schema but without migrations directory
-func setupTestStore(t *testing.T) *Store {
- t.Helper()
-
- // Create temporary database file
- tempDir := t.TempDir()
- dbPath := filepath.Join(tempDir, "test.db")
-
- db, err := sql.Open("sqlite3", dbPath)
- if err != nil {
- t.Fatalf("Failed to open test database: %v", err)
- }
-
- // Enable foreign keys
- if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
- t.Fatalf("Failed to enable foreign keys: %v", err)
- }
-
- // Enable WAL mode for better concurrency
- if _, err := db.Exec("PRAGMA journal_mode = WAL"); err != nil {
- t.Fatalf("Failed to enable WAL mode: %v", err)
- }
-
- // Serialize writes to prevent "database is locked" errors
- db.SetMaxOpenConns(1)
-
- store := &Store{db: db}
-
- // Create notes table directly (without migrations)
- schema := `
- CREATE TABLE IF NOT EXISTS notes (
- filename TEXT PRIMARY KEY,
- title TEXT NOT NULL,
- content TEXT,
- modified DATETIME NOT NULL,
- path TEXT NOT NULL,
- tags TEXT,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
- );
- CREATE INDEX IF NOT EXISTS idx_notes_modified ON notes(modified DESC);
- `
- if _, err := db.Exec(schema); err != nil {
- t.Fatalf("Failed to create schema: %v", err)
- }
-
- return store
-}
-
-// TestGetNotes_LimitClause verifies that the LIMIT clause works correctly
-// and prevents SQL injection
-func TestGetNotes_LimitClause(t *testing.T) {
- store := setupTestStore(t)
- defer store.Close()
-
- // Create 3 distinct notes with different modification times
- baseTime := time.Now()
- notes := []models.Note{
- {
- Filename: "note1.md",
- Title: "Note 1",
- Content: "Content of note 1",
- Modified: baseTime.Add(-2 * time.Hour),
- Path: "/vault/note1.md",
- Tags: []string{"tag1"},
- },
- {
- Filename: "note2.md",
- Title: "Note 2",
- Content: "Content of note 2",
- Modified: baseTime.Add(-1 * time.Hour),
- Path: "/vault/note2.md",
- Tags: []string{"tag2"},
- },
- {
- Filename: "note3.md",
- Title: "Note 3",
- Content: "Content of note 3",
- Modified: baseTime,
- Path: "/vault/note3.md",
- Tags: []string{"tag3"},
- },
- }
-
- // Save all 3 notes
- if err := store.SaveNotes(notes); err != nil {
- t.Fatalf("Failed to save notes: %v", err)
- }
-
- // Test 1: Call GetNotes(2) - should return exactly 2 notes
- t.Run("GetNotes with limit 2", func(t *testing.T) {
- result, err := store.GetNotes(2)
- if err != nil {
- t.Fatalf("GetNotes(2) returned error: %v", err)
- }
-
- if len(result) != 2 {
- t.Errorf("Expected exactly 2 notes, got %d", len(result))
- }
-
- // Verify they are the most recent notes (note3 and note2)
- if len(result) >= 2 {
- if result[0].Filename != "note3.md" {
- t.Errorf("Expected first note to be 'note3.md', got '%s'", result[0].Filename)
- }
- if result[1].Filename != "note2.md" {
- t.Errorf("Expected second note to be 'note2.md', got '%s'", result[1].Filename)
- }
- }
- })
-
- // Test 2: Call GetNotes(5) - should return all 3 notes
- t.Run("GetNotes with limit 5", func(t *testing.T) {
- result, err := store.GetNotes(5)
- if err != nil {
- t.Fatalf("GetNotes(5) returned error: %v", err)
- }
-
- if len(result) != 3 {
- t.Errorf("Expected all 3 notes, got %d", len(result))
- }
-
- // Verify order (most recent first)
- if len(result) == 3 {
- if result[0].Filename != "note3.md" {
- t.Errorf("Expected first note to be 'note3.md', got '%s'", result[0].Filename)
- }
- if result[1].Filename != "note2.md" {
- t.Errorf("Expected second note to be 'note2.md', got '%s'", result[1].Filename)
- }
- if result[2].Filename != "note1.md" {
- t.Errorf("Expected third note to be 'note1.md', got '%s'", result[2].Filename)
- }
- }
- })
-
- // Test 3: Call GetNotes(0) - should return all notes (no limit)
- t.Run("GetNotes with no limit", func(t *testing.T) {
- result, err := store.GetNotes(0)
- if err != nil {
- t.Fatalf("GetNotes(0) returned error: %v", err)
- }
-
- if len(result) != 3 {
- t.Errorf("Expected all 3 notes with no limit, got %d", len(result))
- }
- })
-
- // Test 4: Call GetNotes(1) - should return exactly 1 note
- t.Run("GetNotes with limit 1", func(t *testing.T) {
- result, err := store.GetNotes(1)
- if err != nil {
- t.Fatalf("GetNotes(1) returned error: %v", err)
- }
-
- if len(result) != 1 {
- t.Errorf("Expected exactly 1 note, got %d", len(result))
- }
-
- // Should be the most recent note
- if len(result) == 1 && result[0].Filename != "note3.md" {
- t.Errorf("Expected note to be 'note3.md', got '%s'", result[0].Filename)
- }
- })
-}
-
-// TestGetNotes_EmptyDatabase verifies behavior with empty database
-func TestGetNotes_EmptyDatabase(t *testing.T) {
- store := setupTestStore(t)
- defer store.Close()
-
- result, err := store.GetNotes(10)
- if err != nil {
- t.Fatalf("GetNotes on empty database returned error: %v", err)
- }
-
- if len(result) != 0 {
- t.Errorf("Expected 0 notes from empty database, got %d", len(result))
- }
-}
-
-// TestSaveNotes_Upsert verifies that SaveNotes properly upserts notes
-func TestSaveNotes_Upsert(t *testing.T) {
- store := setupTestStore(t)
- defer store.Close()
-
- baseTime := time.Now()
-
- // Save initial note
- initialNote := []models.Note{
- {
- Filename: "test.md",
- Title: "Initial Title",
- Content: "Initial content",
- Modified: baseTime,
- Path: "/vault/test.md",
- Tags: []string{"initial"},
- },
- }
-
- if err := store.SaveNotes(initialNote); err != nil {
- t.Fatalf("Failed to save initial note: %v", err)
- }
-
- // Verify initial save
- notes, err := store.GetNotes(0)
- if err != nil {
- t.Fatalf("Failed to get notes: %v", err)
- }
- if len(notes) != 1 {
- t.Fatalf("Expected 1 note after initial save, got %d", len(notes))
- }
- if notes[0].Title != "Initial Title" {
- t.Errorf("Expected title 'Initial Title', got '%s'", notes[0].Title)
- }
-
- // Update the same note
- updatedNote := []models.Note{
- {
- Filename: "test.md",
- Title: "Updated Title",
- Content: "Updated content",
- Modified: baseTime.Add(1 * time.Hour),
- Path: "/vault/test.md",
- Tags: []string{"updated"},
- },
- }
-
- if err := store.SaveNotes(updatedNote); err != nil {
- t.Fatalf("Failed to save updated note: %v", err)
- }
-
- // Verify update (should still be 1 note, not 2)
- notes, err = store.GetNotes(0)
- if err != nil {
- t.Fatalf("Failed to get notes after update: %v", err)
- }
- if len(notes) != 1 {
- t.Errorf("Expected 1 note after update (upsert), got %d", len(notes))
- }
- if notes[0].Title != "Updated Title" {
- t.Errorf("Expected title 'Updated Title', got '%s'", notes[0].Title)
- }
-}
-
-// TestGetNotes_NegativeLimit verifies behavior with negative limit
-func TestGetNotes_NegativeLimit(t *testing.T) {
- store := setupTestStore(t)
- defer store.Close()
-
- // Save a note
- notes := []models.Note{
- {
- Filename: "test.md",
- Title: "Test",
- Content: "Test content",
- Modified: time.Now(),
- Path: "/vault/test.md",
- Tags: []string{},
- },
- }
-
- if err := store.SaveNotes(notes); err != nil {
- t.Fatalf("Failed to save note: %v", err)
- }
-
- // Call with negative limit (should be treated as no limit)
- result, err := store.GetNotes(-1)
- if err != nil {
- t.Fatalf("GetNotes(-1) returned error: %v", err)
- }
-
- // Should return all notes since negative is treated as 0/no limit
- if len(result) != 1 {
- t.Errorf("Expected 1 note with negative limit, got %d", len(result))
- }
-}
-
-// TestGetNotes_SQLInjectionAttempt verifies that LIMIT parameter is properly sanitized
-func TestGetNotes_SQLInjectionAttempt(t *testing.T) {
- store := setupTestStore(t)
- defer store.Close()
-
- // Save some notes
- notes := []models.Note{
- {
- Filename: "note1.md",
- Title: "Note 1",
- Content: "Content 1",
- Modified: time.Now(),
- Path: "/vault/note1.md",
- Tags: []string{},
- },
- {
- Filename: "note2.md",
- Title: "Note 2",
- Content: "Content 2",
- Modified: time.Now(),
- Path: "/vault/note2.md",
- Tags: []string{},
- },
- }
-
- if err := store.SaveNotes(notes); err != nil {
- t.Fatalf("Failed to save notes: %v", err)
- }
-
- // The LIMIT parameter is now properly parameterized using sql.Query with args
- // This test verifies that the code uses parameterized queries
- // If it didn't, passing a malicious value could cause SQL injection
-
- // Normal call should work
- result, err := store.GetNotes(1)
- if err != nil {
- t.Fatalf("GetNotes(1) returned error: %v", err)
- }
-
- if len(result) != 1 {
- t.Errorf("Expected 1 note, got %d", len(result))
- }
-
- // The fact that this test passes with a simple integer confirms
- // that the implementation properly uses parameterized queries
- // and is not vulnerable to SQL injection via the LIMIT clause
- fmt.Println("✓ LIMIT clause is properly parameterized")
-}
-
// setupTestStoreWithTasks creates a test store with tasks table
func setupTestStoreWithTasks(t *testing.T) *Store {
t.Helper()