117 lines
4.7 KiB
Go
117 lines
4.7 KiB
Go
package postgres
|
|
|
|
import (
|
|
"database/sql"
|
|
|
|
"gardomatic.kleiax.de/internal/storage"
|
|
)
|
|
|
|
// TaskModel stores garden tasks in PostgreSQL.
|
|
// TaskModel implements storage.TaskModelInterface for garden-scoped work items.
|
|
type TaskModel struct{ DB *sql.DB }
|
|
|
|
const taskColumns = `id, garden_id, plant_id, location_id, template_id, title,
|
|
description, due_at_start, due_at_end, generated_for, recurrence, recurrence_interval, repeat_from_id, completed_at, completed_by,
|
|
priority, active, created_by, created_at, updated_at, version, plant_status_on_completion`
|
|
|
|
func scanTask(s scanner) (storage.Task, error) {
|
|
var task storage.Task
|
|
err := s.Scan(
|
|
&task.ID, &task.GardenID, &task.PlantID, &task.LocationID, &task.TemplateID,
|
|
&task.Title, &task.Description, &task.DueAtStart, &task.DueAtEnd,
|
|
&task.GeneratedFor, &task.Recurrence, &task.RecurrenceInterval, &task.RepeatFromID, &task.CompletedAt, &task.CompletedBy, &task.Priority, &task.Active,
|
|
&task.CreatedBy, &task.CreatedAt, &task.UpdatedAt, &task.Version, &task.PlantStatusOnCompletion,
|
|
)
|
|
return task, err
|
|
}
|
|
|
|
// Insert creates a task; generated task slots remain idempotent under concurrency.
|
|
func (m TaskModel) Insert(task storage.Task) (storage.Task, error) {
|
|
if task.RecurrenceInterval < 1 {
|
|
task.RecurrenceInterval = 1
|
|
}
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
err := m.DB.QueryRowContext(ctx, `
|
|
INSERT INTO tasks (
|
|
garden_id, plant_id, location_id, template_id, title, description,
|
|
due_at_start, due_at_end, generated_for, recurrence, recurrence_interval, repeat_from_id, completed_at, completed_by,
|
|
priority, active, created_by, plant_status_on_completion)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
|
RETURNING id, created_at, updated_at, version`,
|
|
task.GardenID, task.PlantID, task.LocationID, task.TemplateID, task.Title,
|
|
task.Description, task.DueAtStart, task.DueAtEnd, task.GeneratedFor,
|
|
task.Recurrence, task.RecurrenceInterval, task.RepeatFromID, task.CompletedAt, task.CompletedBy, task.Priority, task.Active, task.CreatedBy, task.PlantStatusOnCompletion,
|
|
).Scan(&task.ID, &task.CreatedAt, &task.UpdatedAt, &task.Version)
|
|
if err != nil {
|
|
return storage.Task{}, recordError(err)
|
|
}
|
|
return task, nil
|
|
}
|
|
|
|
// Get returns a task within its garden.
|
|
func (m TaskModel) Get(gardenID, id int) (storage.Task, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
task, err := scanTask(m.DB.QueryRowContext(ctx, `SELECT `+taskColumns+` FROM tasks WHERE garden_id = $1 AND id = $2`, gardenID, id))
|
|
if err != nil {
|
|
return storage.Task{}, recordError(err)
|
|
}
|
|
return task, nil
|
|
}
|
|
|
|
// GetAllForGarden lists tasks in a garden.
|
|
func (m TaskModel) GetAllForGarden(gardenID int) ([]storage.Task, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
rows, err := m.DB.QueryContext(ctx, `SELECT `+taskColumns+`
|
|
FROM tasks WHERE garden_id = $1
|
|
ORDER BY completed_at NULLS FIRST, due_at_end NULLS LAST, priority DESC, id`, gardenID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
tasks := []storage.Task{}
|
|
for rows.Next() {
|
|
task, err := scanTask(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tasks = append(tasks, task)
|
|
}
|
|
return tasks, rows.Err()
|
|
}
|
|
|
|
// Update changes a task using optimistic locking.
|
|
func (m TaskModel) Update(gardenID int, task storage.Task) (storage.Task, error) {
|
|
if task.RecurrenceInterval < 1 {
|
|
task.RecurrenceInterval = 1
|
|
}
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
err := m.DB.QueryRowContext(ctx, `
|
|
UPDATE tasks SET plant_id = $1, location_id = $2, template_id = $3,
|
|
title = $4, description = $5, due_at_start = $6, due_at_end = $7,
|
|
generated_for = $8, recurrence = $9, recurrence_interval = $10, repeat_from_id = $11, completed_at = $12, completed_by = $13,
|
|
priority = $14, active = $15, plant_status_on_completion=$16, updated_at = CURRENT_TIMESTAMP, version = version + 1
|
|
WHERE garden_id = $17 AND id = $18 AND version = $19
|
|
RETURNING updated_at, version`,
|
|
task.PlantID, task.LocationID, task.TemplateID, task.Title, task.Description,
|
|
task.DueAtStart, task.DueAtEnd, task.GeneratedFor, task.Recurrence, task.RecurrenceInterval,
|
|
task.RepeatFromID, task.CompletedAt, task.CompletedBy, task.Priority, task.Active, task.PlantStatusOnCompletion, gardenID, task.ID, task.Version,
|
|
).Scan(&task.UpdatedAt, &task.Version)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return storage.Task{}, storage.ErrEditConflict
|
|
}
|
|
return storage.Task{}, err
|
|
}
|
|
return task, nil
|
|
}
|
|
|
|
// Delete removes a task within its garden.
|
|
func (m TaskModel) Delete(gardenID, id int) error {
|
|
return deleteByID(m.DB, `DELETE FROM tasks WHERE garden_id = $1 AND id = $2`, gardenID, id)
|
|
}
|