summaryrefslogtreecommitdiff
path: root/internal/store
diff options
context:
space:
mode:
Diffstat (limited to 'internal/store')
-rw-r--r--internal/store/native_tasks.go21
-rw-r--r--internal/store/sqlite.go241
-rw-r--r--internal/store/sqlite_test.go260
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)
}