diff options
Diffstat (limited to 'internal/store')
| -rw-r--r-- | internal/store/native_tasks.go | 21 | ||||
| -rw-r--r-- | internal/store/sqlite.go | 241 | ||||
| -rw-r--r-- | internal/store/sqlite_test.go | 260 |
3 files changed, 20 insertions, 502 deletions
diff --git a/internal/store/native_tasks.go b/internal/store/native_tasks.go index ca19377..5758407 100644 --- a/internal/store/native_tasks.go +++ b/internal/store/native_tasks.go @@ -2,7 +2,6 @@ package store import ( "encoding/json" - "log" "time" "task-dashboard/internal/models" @@ -96,26 +95,6 @@ func (s *Store) UncompleteNativeTask(id string) error { return err } -// ImportFromTodoist copies all incomplete tasks from the Todoist cache into native_tasks. -// Skips tasks already present (by ID). Returns the count of newly imported tasks. -func (s *Store) ImportFromTodoist() (int, error) { - result, err := s.db.Exec(` - INSERT OR IGNORE INTO native_tasks - (id, content, description, project_name, due_date, priority, completed, labels, created_at, updated_at) - SELECT - id, content, COALESCE(description, ''), COALESCE(project_name, ''), - due_date, priority, 0, COALESCE(labels, '[]'), created_at, CURRENT_TIMESTAMP - FROM tasks - WHERE completed = 0 - `) - if err != nil { - return 0, err - } - n, _ := result.RowsAffected() - log.Printf("ImportFromTodoist: imported %d tasks", n) - return int(n), nil -} - func scanNativeTasks(rows interface{ Next() bool; Scan(...interface{}) error; Err() error }) ([]models.Task, error) { var tasks []models.Task for rows.Next() { diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go index f9c9a6b..a6e9fd4 100644 --- a/internal/store/sqlite.go +++ b/internal/store/sqlite.go @@ -2,7 +2,6 @@ package store import ( "database/sql" - "encoding/json" "errors" "fmt" "log" @@ -19,11 +18,10 @@ import ( // Cache key constants const ( - CacheKeyTodoistTasks = "todoist_tasks" - CacheKeyTrelloBoards = "trello_boards" - CacheKeyPlanToEatMeals = "plantoeat_meals" - CacheKeyGoogleCalendar = "google_calendar" - CacheKeyGoogleTasks = "google_tasks" + CacheKeyTrelloBoards = "trello_boards" + CacheKeyPlanToEatMeals = "plantoeat_meals" + CacheKeyGoogleCalendar = "google_calendar" + CacheKeyGoogleTasks = "google_tasks" ) type Store struct { @@ -139,177 +137,6 @@ func (s *Store) runMigrations() error { return nil } -// Tasks operations - -// scanTask scans a single task row from the database -func scanTask(rows *sql.Rows) (models.Task, error) { - var task models.Task - var labelsJSON string - var dueDate sql.NullTime - - err := rows.Scan( - &task.ID, - &task.Content, - &task.Description, - &task.ProjectID, - &task.ProjectName, - &dueDate, - &task.Priority, - &task.Completed, - &labelsJSON, - &task.URL, - &task.CreatedAt, - ) - if err != nil { - return task, err - } - - if dueDate.Valid { - task.DueDate = &dueDate.Time - } - - if err := json.Unmarshal([]byte(labelsJSON), &task.Labels); err != nil { - log.Printf("Warning: failed to unmarshal labels for task %s: %v", task.ID, err) - task.Labels = []string{} - } - return task, nil -} - -// scanTasks scans multiple task rows from the database -func scanTasks(rows *sql.Rows) ([]models.Task, error) { - var tasks []models.Task - for rows.Next() { - task, err := scanTask(rows) - if err != nil { - return nil, err - } - tasks = append(tasks, task) - } - return tasks, rows.Err() -} - -// SaveTasks saves multiple tasks to the database -func (s *Store) SaveTasks(tasks []models.Task) error { - tx, err := s.db.Begin() - if err != nil { - return err - } - defer func() { _ = tx.Rollback() }() - - // Clear existing tasks first to remove stale data - if _, err := tx.Exec(`DELETE FROM tasks`); err != nil { - return err - } - - stmt, err := tx.Prepare(` - INSERT OR REPLACE INTO tasks - (id, content, description, project_id, project_name, due_date, priority, completed, labels, url, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) - `) - if err != nil { - return err - } - defer func() { _ = stmt.Close() }() - - for _, task := range tasks { - labelsJSON, err := json.Marshal(task.Labels) - if err != nil { - log.Printf("Warning: failed to marshal labels for task %s: %v", task.ID, err) - labelsJSON = []byte("[]") - } - _, err = stmt.Exec( - task.ID, - task.Content, - task.Description, - task.ProjectID, - task.ProjectName, - task.DueDate, - task.Priority, - task.Completed, - string(labelsJSON), - task.URL, - task.CreatedAt, - ) - if err != nil { - return err - } - } - - return tx.Commit() -} - -// GetTasks retrieves all tasks from the database -func (s *Store) GetTasks() ([]models.Task, error) { - rows, err := s.db.Query(` - SELECT id, content, description, project_id, project_name, due_date, priority, completed, labels, url, created_at - FROM tasks - ORDER BY completed ASC, CASE WHEN due_date IS NULL THEN 1 ELSE 0 END, due_date ASC, priority DESC - `) - if err != nil { - return nil, err - } - defer func() { _ = rows.Close() }() - return scanTasks(rows) -} - -// DeleteTask removes a task from the cache by ID -func (s *Store) DeleteTask(id string) error { - _, err := s.db.Exec(`DELETE FROM tasks WHERE id = ?`, id) - return err -} - -// UpsertTask inserts or updates a single task -func (s *Store) UpsertTask(task models.Task) error { - labelsJSON, err := json.Marshal(task.Labels) - if err != nil { - log.Printf("Warning: failed to marshal labels for task %s: %v", task.ID, err) - labelsJSON = []byte("[]") - } - _, err = s.db.Exec(` - INSERT OR REPLACE INTO tasks - (id, content, description, project_id, project_name, due_date, priority, completed, labels, url, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) - `, - task.ID, - task.Content, - task.Description, - task.ProjectID, - task.ProjectName, - task.DueDate, - task.Priority, - task.Completed, - string(labelsJSON), - task.URL, - task.CreatedAt, - ) - return err -} - -// DeleteTasksByIDs removes multiple tasks by ID -func (s *Store) DeleteTasksByIDs(ids []string) error { - if len(ids) == 0 { - return nil - } - tx, err := s.db.Begin() - if err != nil { - return err - } - defer func() { _ = tx.Rollback() }() - - stmt, err := tx.Prepare(`DELETE FROM tasks WHERE id = ?`) - if err != nil { - return err - } - defer func() { _ = stmt.Close() }() - - for _, id := range ids { - if _, err := stmt.Exec(id); err != nil { - return err - } - } - return tx.Commit() -} - // Meals operations // SaveMeals saves multiple meals to the database @@ -707,38 +534,6 @@ func (s *Store) GetShoppingItemChecks(source string) (map[string]bool, error) { return checks, rows.Err() } -// GetTasksByDateRange retrieves tasks due within a specific date range, -// including overdue tasks (due before start) so they appear in the timeline. -func (s *Store) GetTasksByDateRange(start, end time.Time) ([]models.Task, error) { - rows, err := s.db.Query(` - SELECT id, content, description, project_id, project_name, due_date, priority, completed, labels, url, created_at - FROM tasks - WHERE due_date IS NOT NULL AND due_date <= ? - AND completed = FALSE - ORDER BY due_date ASC, priority DESC - `, end) - if err != nil { - return nil, err - } - defer func() { _ = rows.Close() }() - return scanTasks(rows) -} - -// GetUndatedTasks retrieves incomplete tasks with no due date. -func (s *Store) GetUndatedTasks() ([]models.Task, error) { - rows, err := s.db.Query(` - SELECT id, content, description, project_id, project_name, due_date, priority, completed, labels, url, created_at - FROM tasks - WHERE due_date IS NULL AND completed = FALSE - ORDER BY priority DESC, created_at ASC - `) - if err != nil { - return nil, err - } - defer func() { _ = rows.Close() }() - return scanTasks(rows) -} - // GetMealsByDateRange retrieves meals within a specific date range func (s *Store) GetMealsByDateRange(start, end time.Time) ([]models.Meal, error) { return s.GetMeals(start, end) @@ -1508,35 +1303,11 @@ func (s *Store) SyncSourceConfigs(source, itemType string, items []models.Source // InvalidateAllCaches removes cache metadata for all known cache keys func (s *Store) InvalidateAllCaches() error { - _, err := s.db.Exec(`DELETE FROM cache_metadata WHERE key IN (?, ?, ?, ?, ?)`, - CacheKeyTodoistTasks, CacheKeyTrelloBoards, CacheKeyPlanToEatMeals, CacheKeyGoogleCalendar, CacheKeyGoogleTasks) + _, err := s.db.Exec(`DELETE FROM cache_metadata WHERE key IN (?, ?, ?, ?)`, + CacheKeyTrelloBoards, CacheKeyPlanToEatMeals, CacheKeyGoogleCalendar, CacheKeyGoogleTasks) return err } -// GetProjectsFromTasks returns distinct projects from the tasks table -func (s *Store) GetProjectsFromTasks() ([]models.Project, error) { - rows, err := s.db.Query(` - SELECT DISTINCT project_id, project_name - FROM tasks - WHERE project_id != '' - ORDER BY project_name - `) - if err != nil { - return nil, err - } - defer func() { _ = rows.Close() }() - - var projects []models.Project - for rows.Next() { - var p models.Project - if err := rows.Scan(&p.ID, &p.Name); err != nil { - return nil, err - } - projects = append(projects, p) - } - return projects, rows.Err() -} - // Feature toggles // GetFeatureToggles returns all feature toggles diff --git a/internal/store/sqlite_test.go b/internal/store/sqlite_test.go index 0514eeb..2467f96 100644 --- a/internal/store/sqlite_test.go +++ b/internal/store/sqlite_test.go @@ -78,45 +78,6 @@ func TestRunMigrations_IdempotentOnRestart(t *testing.T) { store2.Close() } -// setupTestStoreWithTasks creates a test store with tasks table -func setupTestStoreWithTasks(t *testing.T) *Store { - t.Helper() - - 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) - } - - db.SetMaxOpenConns(1) - - store := &Store{db: db} - - schema := ` - CREATE TABLE IF NOT EXISTS tasks ( - id TEXT PRIMARY KEY, - content TEXT NOT NULL, - description TEXT, - project_id TEXT, - project_name TEXT, - due_date DATETIME, - priority INTEGER DEFAULT 1, - completed BOOLEAN DEFAULT FALSE, - labels TEXT, - url TEXT, - created_at DATETIME, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); - ` - if _, err := db.Exec(schema); err != nil { - t.Fatalf("Failed to create schema: %v", err) - } - - return store -} - // setupTestStoreWithCards creates a test store with boards and cards tables func setupTestStoreWithCards(t *testing.T) *Store { t.Helper() @@ -190,106 +151,6 @@ func setupTestStoreWithMeals(t *testing.T) *Store { return store } -// TestDeleteTask verifies that DeleteTask removes a task from the cache -func TestDeleteTask(t *testing.T) { - store := setupTestStoreWithTasks(t) - defer func() { _ = store.Close() }() - - // Save some tasks - tasks := []models.Task{ - { - ID: "task1", - Content: "Task 1", - Description: "Description 1", - Priority: 1, - Completed: false, - Labels: []string{}, - CreatedAt: time.Now(), - }, - { - ID: "task2", - Content: "Task 2", - Description: "Description 2", - Priority: 2, - Completed: false, - Labels: []string{}, - CreatedAt: time.Now(), - }, - { - ID: "task3", - Content: "Task 3", - Description: "Description 3", - Priority: 3, - Completed: false, - Labels: []string{}, - CreatedAt: time.Now(), - }, - } - - if err := store.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save tasks: %v", err) - } - - // Verify all 3 tasks exist - result, err := store.GetTasks() - if err != nil { - t.Fatalf("Failed to get tasks: %v", err) - } - if len(result) != 3 { - t.Fatalf("Expected 3 tasks, got %d", len(result)) - } - - // Delete task2 - if err := store.DeleteTask("task2"); err != nil { - t.Fatalf("DeleteTask failed: %v", err) - } - - // Verify only 2 tasks remain - result, err = store.GetTasks() - if err != nil { - t.Fatalf("Failed to get tasks after delete: %v", err) - } - if len(result) != 2 { - t.Errorf("Expected 2 tasks after delete, got %d", len(result)) - } - - // Verify task2 is gone - for _, task := range result { - if task.ID == "task2" { - t.Errorf("task2 should have been deleted but was found") - } - } - - // Verify task1 and task3 still exist - foundTask1, foundTask3 := false, false - for _, task := range result { - if task.ID == "task1" { - foundTask1 = true - } - if task.ID == "task3" { - foundTask3 = true - } - } - if !foundTask1 { - t.Error("task1 should still exist") - } - if !foundTask3 { - t.Error("task3 should still exist") - } -} - -// TestDeleteTask_NonExistent verifies that deleting a non-existent task doesn't error -func TestDeleteTask_NonExistent(t *testing.T) { - store := setupTestStoreWithTasks(t) - defer func() { _ = store.Close() }() - - // Delete a task that doesn't exist - should not error - err := store.DeleteTask("nonexistent") - if err != nil { - t.Errorf("DeleteTask on non-existent task should not error, got: %v", err) - } -} - // TestDeleteCard verifies that DeleteCard removes a card from the cache func TestDeleteCard(t *testing.T) { store := setupTestStoreWithCards(t) @@ -532,99 +393,6 @@ func TestSaveAndGetBoards_ManyBoards(t *testing.T) { } } -func TestGetTasksByDateRange(t *testing.T) { - store := setupTestStoreWithTasks(t) - defer func() { _ = store.Close() }() - - now := time.Now() - tomorrow := now.Add(24 * time.Hour) - nextWeek := now.Add(7 * 24 * time.Hour) - - tasks := []models.Task{ - {ID: "1", Content: "Task 1", DueDate: &now}, - {ID: "2", Content: "Task 2", DueDate: &tomorrow}, - {ID: "3", Content: "Task 3", DueDate: &nextWeek}, - } - - if err := store.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save tasks: %v", err) - } - - // Test range covering today and tomorrow — should include all tasks <= end - start := now.Add(-1 * time.Hour) - end := tomorrow.Add(1 * time.Hour) - - results, err := store.GetTasksByDateRange(start, end) - if err != nil { - t.Fatalf("GetTasksByDateRange failed: %v", err) - } - - if len(results) != 2 { - t.Errorf("Expected 2 tasks (today + tomorrow, not next week), got %d", len(results)) - } -} - -func TestGetTasksByDateRange_IncludesOverdue(t *testing.T) { - store := setupTestStoreWithTasks(t) - defer func() { _ = store.Close() }() - - now := time.Now() - yesterday := now.Add(-24 * time.Hour) - tomorrow := now.Add(24 * time.Hour) - - tasks := []models.Task{ - {ID: "overdue", Content: "Overdue", DueDate: &yesterday}, - {ID: "current", Content: "Current", DueDate: &now}, - } - - if err := store.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save tasks: %v", err) - } - - // Query from "today" onward — overdue tasks (before start) should also be included - results, err := store.GetTasksByDateRange(now, tomorrow) - if err != nil { - t.Fatalf("GetTasksByDateRange failed: %v", err) - } - - if len(results) != 2 { - t.Errorf("Expected 2 tasks (overdue + current), got %d", len(results)) - for _, r := range results { - t.Logf(" task: %s due=%v", r.Content, r.DueDate) - } - } -} - -func TestGetTasksByDateRange_ExcludesCompleted(t *testing.T) { - store := setupTestStoreWithTasks(t) - defer func() { _ = store.Close() }() - - now := time.Now() - yesterday := now.Add(-24 * time.Hour) - tomorrow := now.Add(24 * time.Hour) - - tasks := []models.Task{ - {ID: "done", Content: "Completed overdue", DueDate: &yesterday, Completed: true}, - {ID: "active", Content: "Active", DueDate: &now}, - } - - if err := store.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save tasks: %v", err) - } - - results, err := store.GetTasksByDateRange(now, tomorrow) - if err != nil { - t.Fatalf("GetTasksByDateRange failed: %v", err) - } - - if len(results) != 1 { - t.Errorf("Expected 1 task (only active), got %d", len(results)) - } - if len(results) > 0 && results[0].ID != "active" { - t.Errorf("Expected active task, got %s", results[0].ID) - } -} - func TestGetMealsByDateRange(t *testing.T) { store := setupTestStoreWithMeals(t) defer func() { _ = store.Close() }() @@ -982,7 +750,7 @@ func TestCompletedTasks_SaveAndGet(t *testing.T) { now := time.Now() // Save completed task with due date - if err := store.SaveCompletedTask("todoist", "task-123", "Buy groceries", &now); err != nil { + if err := store.SaveCompletedTask("doot", "task-123", "Buy groceries", &now); err != nil { t.Fatalf("Failed to save completed task: %v", err) } @@ -1001,17 +769,17 @@ func TestCompletedTasks_SaveAndGet(t *testing.T) { } // Verify task data - var todoistTask models.CompletedTask + var dootTask models.CompletedTask for _, task := range tasks { - if task.Source == "todoist" { - todoistTask = task + if task.Source == "doot" { + dootTask = task break } } - if todoistTask.Title != "Buy groceries" { - t.Errorf("Expected title 'Buy groceries', got '%s'", todoistTask.Title) + if dootTask.Title != "Buy groceries" { + t.Errorf("Expected title 'Buy groceries', got '%s'", dootTask.Title) } - if todoistTask.DueDate == nil { + if dootTask.DueDate == nil { t.Error("Expected due date to be set") } } @@ -1022,7 +790,7 @@ func TestCompletedTasks_Limit(t *testing.T) { // Save multiple tasks for i := 0; i < 10; i++ { - _ = store.SaveCompletedTask("todoist", "task-"+string(rune('0'+i)), "Task "+string(rune('0'+i)), nil) + _ = store.SaveCompletedTask("doot", "task-"+string(rune('0'+i)), "Task "+string(rune('0'+i)), nil) } // Get with limit @@ -1043,7 +811,7 @@ func TestGetCompletedTasks_CompletedAtIsParsed(t *testing.T) { knownTimestamp := "2026-03-15 14:30:00" _, err := s.db.Exec( `INSERT INTO completed_tasks (source, source_id, title, completed_at) VALUES (?, ?, ?, ?)`, - "todoist", "task-ts-test", "Timestamp Test Task", knownTimestamp, + "doot", "task-ts-test", "Timestamp Test Task", knownTimestamp, ) if err != nil { t.Fatalf("Failed to insert task: %v", err) @@ -1329,7 +1097,7 @@ func TestSyncTokens_SetGetClear(t *testing.T) { defer func() { _ = store.Close() }() // Get non-existent token - token, err := store.GetSyncToken("todoist") + token, err := store.GetSyncToken("trello") if err != nil { t.Fatalf("Failed to get token: %v", err) } @@ -1338,12 +1106,12 @@ func TestSyncTokens_SetGetClear(t *testing.T) { } // Set token - if err := store.SetSyncToken("todoist", "sync-token-123"); err != nil { + if err := store.SetSyncToken("trello", "sync-token-123"); err != nil { t.Fatalf("Failed to set token: %v", err) } // Get token - token, err = store.GetSyncToken("todoist") + token, err = store.GetSyncToken("trello") if err != nil { t.Fatalf("Failed to get token after set: %v", err) } @@ -1352,11 +1120,11 @@ func TestSyncTokens_SetGetClear(t *testing.T) { } // Clear token - if err := store.ClearSyncToken("todoist"); err != nil { + if err := store.ClearSyncToken("trello"); err != nil { t.Fatalf("Failed to clear token: %v", err) } - token, _ = store.GetSyncToken("todoist") + token, _ = store.GetSyncToken("trello") if token != "" { t.Errorf("Expected empty token after clear, got '%s'", token) } |
