internal/storage: SQLite-БД для задач и трассировок (таски + статус-машина, трассы с группировкой по task_id)
All checks were successful
build-test / build (push) Successful in 30s
All checks were successful
build-test / build (push) Successful in 30s
This commit is contained in:
138
internal/storage/tasks.go
Normal file
138
internal/storage/tasks.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CreateTask создаёт задачу со статусом draft. Возвращает её ID.
|
||||
// chat_id и task_tag передаются извне (в диалоге — chat.Address + uuid).
|
||||
func (s *Storage) CreateTask(ctx context.Context, t *Task) (int64, error) {
|
||||
now := Now()
|
||||
res, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO tasks (chat_id, title, goal, repo, why, ac, task_tag, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
t.ChatID, t.Title, t.Goal, t.Repo, t.Why, t.AC,
|
||||
t.TaskTag, StatusDraft, now, now,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%w: create task: %w", ErrDB, err)
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%w: last insert id: %w", ErrDB, err)
|
||||
}
|
||||
t.ID = id
|
||||
t.Status = StatusDraft
|
||||
t.CreatedAt = now
|
||||
t.UpdatedAt = now
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// GetTask возвращает задачу по ID.
|
||||
func (s *Storage) GetTask(ctx context.Context, id int64) (*Task, error) {
|
||||
t := &Task{}
|
||||
err := s.db.QueryRowContext(ctx, `
|
||||
SELECT id, chat_id, title, goal, repo, why, ac, task_tag, status, created_at, updated_at
|
||||
FROM tasks WHERE id = ?`, id).Scan(
|
||||
&t.ID, &t.ChatID, &t.Title, &t.Goal, &t.Repo,
|
||||
&t.Why, &t.AC, &t.TaskTag, &t.Status, &t.CreatedAt, &t.UpdatedAt,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("%w: task %d", ErrNotFound, id)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: get task %d: %w", ErrDB, id, err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// UpdateTask обновляет поля задачи. Если newStatus указан и отличен от текущего —
|
||||
// проверяет валидность перехода. Возвращает ошибку ErrInvalidStatus при недопустимом переходе.
|
||||
func (s *Storage) UpdateTask(ctx context.Context, t *Task) error {
|
||||
// получаем текущий статус для валидации перехода
|
||||
var oldStatus Status
|
||||
err := s.db.QueryRowContext(ctx, `SELECT status FROM tasks WHERE id = ?`, t.ID).Scan(&oldStatus)
|
||||
if err == sql.ErrNoRows {
|
||||
return fmt.Errorf("%w: task %d", ErrNotFound, t.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: get status %d: %w", ErrDB, t.ID, err)
|
||||
}
|
||||
|
||||
if t.Status != "" && t.Status != oldStatus {
|
||||
if !IsValidTransition(oldStatus, t.Status) {
|
||||
return fmt.Errorf("%w: %s → %s for task %d", ErrInvalidStatus, oldStatus, t.Status, t.ID)
|
||||
}
|
||||
} else if t.Status == "" {
|
||||
t.Status = oldStatus
|
||||
}
|
||||
|
||||
now := Now()
|
||||
res, err := s.db.ExecContext(ctx, `
|
||||
UPDATE tasks
|
||||
SET title=?, goal=?, repo=?, why=?, ac=?, status=?, updated_at=?
|
||||
WHERE id=?`,
|
||||
t.Title, t.Goal, t.Repo, t.Why, t.AC, t.Status, now, t.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: update task %d: %w", ErrDB, t.ID, err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("%w: task %d", ErrNotFound, t.ID)
|
||||
}
|
||||
t.UpdatedAt = now
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListTasks возвращает список задач с фильтрацией.
|
||||
func (s *Storage) ListTasks(ctx context.Context, filter TaskFilter) ([]*Task, error) {
|
||||
if filter.Limit <= 0 {
|
||||
filter.Limit = 50
|
||||
}
|
||||
where := "1=1"
|
||||
args := []any{}
|
||||
if filter.ChatID != "" {
|
||||
where += " AND chat_id = ?"
|
||||
args = append(args, filter.ChatID)
|
||||
}
|
||||
if filter.Status != "" {
|
||||
where += " AND status = ?"
|
||||
args = append(args, string(filter.Status))
|
||||
}
|
||||
args = append(args, filter.Limit, filter.Offset)
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT id, chat_id, title, goal, repo, why, ac, task_tag, status, created_at, updated_at
|
||||
FROM tasks WHERE `+where+` ORDER BY updated_at DESC LIMIT ? OFFSET ?`, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: list tasks: %w", ErrDB, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tasks []*Task
|
||||
for rows.Next() {
|
||||
t := &Task{}
|
||||
if err := rows.Scan(&t.ID, &t.ChatID, &t.Title, &t.Goal, &t.Repo,
|
||||
&t.Why, &t.AC, &t.TaskTag, &t.Status, &t.CreatedAt, &t.UpdatedAt); err != nil {
|
||||
return nil, fmt.Errorf("%w: scan task: %w", ErrDB, err)
|
||||
}
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
return tasks, rows.Err()
|
||||
}
|
||||
|
||||
// DeleteTask удаляет задачу (каскадно — трассы). Только для тестов/админки.
|
||||
func (s *Storage) DeleteTask(ctx context.Context, id int64) error {
|
||||
res, err := s.db.ExecContext(ctx, `DELETE FROM tasks WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: delete task %d: %w", ErrDB, id, err)
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("%w: task %d", ErrNotFound, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user