Files
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

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)
}