1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
package store
import (
"database/sql"
"encoding/json"
"errors"
"time"
"task-dashboard/internal/models"
)
// ErrNativeTaskNotFound is returned by CompleteNativeTask, UncompleteNativeTask,
// and RescheduleNativeTask when no row matches the given id -- previously these
// three silently reported success on a 0-row UPDATE (Exec's err is nil even when
// no rows match), so a stale or wrong id from a caller looked identical to a real
// completion: the HTTP response was 200, but nothing in the database changed.
var ErrNativeTaskNotFound = errors.New("native task not found")
// GetNativeTasks returns all non-completed native tasks.
func (s *Store) GetNativeTasks() ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at
FROM native_tasks
WHERE completed = 0
ORDER BY 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 scanNativeTasks(rows)
}
// GetNativeTasksByDateRange returns non-completed native tasks due within the given range.
func (s *Store) GetNativeTasksByDateRange(start, end time.Time) ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at
FROM native_tasks
WHERE completed = 0 AND due_date IS NOT NULL AND due_date >= ? AND due_date < ?
ORDER BY due_date ASC, priority DESC
`, start, end)
if err != nil {
return nil, err
}
defer func() { _ = rows.Close() }()
return scanNativeTasks(rows)
}
// GetOverdueNativeTasks returns non-completed native tasks whose due date is
// before the given time. BuildTimeline calls this alongside
// GetNativeTasksByDateRange, whose lower bound excludes anything due before
// the requested range's start -- without this, a task overdue from a
// previous day never gets fetched at all, so it never reaches
// ComputeDaySection to be marked IsOverdue.
func (s *Store) GetOverdueNativeTasks(before time.Time) ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at
FROM native_tasks
WHERE completed = 0 AND due_date IS NOT NULL AND due_date < ?
ORDER BY due_date ASC, priority DESC
`, before)
if err != nil {
return nil, err
}
defer func() { _ = rows.Close() }()
return scanNativeTasks(rows)
}
// GetUndatedNativeTasks returns non-completed native tasks with no due date.
func (s *Store) GetUndatedNativeTasks() ([]models.Task, error) {
rows, err := s.db.Query(`
SELECT id, content, description, project_name, due_date, priority, completed, labels, created_at
FROM native_tasks
WHERE completed = 0 AND due_date IS NULL
ORDER BY priority DESC, created_at ASC
`)
if err != nil {
return nil, err
}
defer func() { _ = rows.Close() }()
return scanNativeTasks(rows)
}
// CreateNativeTask inserts a new native task.
func (s *Store) CreateNativeTask(task models.Task) error {
labelsJSON, _ := json.Marshal(task.Labels)
_, err := s.db.Exec(`
INSERT INTO native_tasks (id, content, description, project_name, due_date, priority, labels, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, task.ID, task.Content, task.Description, task.ProjectName, task.DueDate, task.Priority, string(labelsJSON))
return err
}
// UpdateNativeTask updates a native task's content and description.
func (s *Store) UpdateNativeTask(id, content, description string) error {
_, err := s.db.Exec(`
UPDATE native_tasks SET content = ?, description = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, content, description, id)
return err
}
// CompleteNativeTask marks a task as completed. Returns ErrNativeTaskNotFound
// if id doesn't match any row.
func (s *Store) CompleteNativeTask(id string) error {
result, err := s.db.Exec(`
UPDATE native_tasks SET completed = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`, id)
if err != nil {
return err
}
return checkRowsAffected(result)
}
// RescheduleNativeTask sets a new due date on a task. Returns
// ErrNativeTaskNotFound if id doesn't match any row.
func (s *Store) RescheduleNativeTask(id string, dueDate time.Time) error {
result, err := s.db.Exec(`
UPDATE native_tasks SET due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`, dueDate, id)
if err != nil {
return err
}
return checkRowsAffected(result)
}
// UncompleteNativeTask marks a task as not completed. Returns
// ErrNativeTaskNotFound if id doesn't match any row.
func (s *Store) UncompleteNativeTask(id string) error {
result, err := s.db.Exec(`
UPDATE native_tasks SET completed = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`, id)
if err != nil {
return err
}
return checkRowsAffected(result)
}
// checkRowsAffected returns ErrNativeTaskNotFound if the update matched no
// rows -- mirrors the RowsAffected() check already used in sqlite.go's
// ApproveAgentSession/DenyAgentSession for the same "silent 0-row update"
// class of bug.
func checkRowsAffected(result sql.Result) error {
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return ErrNativeTaskNotFound
}
return nil
}
func scanNativeTasks(rows interface {
Next() bool
Scan(...interface{}) error
Err() error
}) ([]models.Task, error) {
var tasks []models.Task
for rows.Next() {
var t models.Task
var labelsJSON string
var dueDateStr *string
if err := rows.Scan(&t.ID, &t.Content, &t.Description, &t.ProjectName, &dueDateStr, &t.Priority, &t.Completed, &labelsJSON, &t.CreatedAt); err != nil {
return nil, err
}
if dueDateStr != nil {
if parsed, err := time.Parse(time.RFC3339, *dueDateStr); err == nil {
t.DueDate = &parsed
} else if parsed, err := time.Parse("2006-01-02 15:04:05", *dueDateStr); err == nil {
t.DueDate = &parsed
} else if parsed, err := time.Parse("2006-01-02", *dueDateStr); err == nil {
t.DueDate = &parsed
}
}
if err := json.Unmarshal([]byte(labelsJSON), &t.Labels); err != nil {
t.Labels = nil
}
tasks = append(tasks, t)
}
return tasks, rows.Err()
}
|