Files
Gardomatic/internal/storage/postgres/plants.go
T
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

105 lines
4.2 KiB
Go

package postgres
import (
"database/sql"
"gardomatic.kleiax.de/internal/storage"
)
// PlantModel stores garden-scoped plant instances in PostgreSQL.
// PlantModel implements storage.PlantModelInterface for PostgreSQL and scopes
// every concrete plant operation to its garden.
type PlantModel struct{ DB *sql.DB }
const plantColumns = `p.id, p.garden_id, p.species_id, p.name, p.notes, p.acquired_at, p.status,
p.removed_at, p.attributes, COALESCE('data:'||i.media_type||';base64,'||replace(encode(i.data,'base64'), E'\n', ''), ''), p.image_id, p.created_at, p.updated_at, p.version,
COALESCE(p.created_by, 0), COALESCE(p.updated_by, 0), COALESCE(p.planted_by, 0), COALESCE(u.name, '')`
func scanPlant(s scanner) (storage.Plant, error) {
var plant storage.Plant
err := s.Scan(
&plant.ID, &plant.GardenID, &plant.SpeciesID, &plant.Name, &plant.Notes,
&plant.AcquiredAt, &plant.Status, &plant.RemovedAt, &plant.Attributes, &plant.ImageData, &plant.ImageID,
&plant.CreatedAt, &plant.UpdatedAt, &plant.Version, &plant.CreatedBy, &plant.UpdatedBy, &plant.PlantedBy, &plant.PlantedByName,
)
return plant, err
}
// Insert creates a plant in its garden.
func (m PlantModel) Insert(plant storage.Plant) (storage.Plant, error) {
ctx, cancel := contextWithTimeout()
defer cancel()
err := m.DB.QueryRowContext(ctx, `
INSERT INTO plants
(garden_id, species_id, name, notes, acquired_at, status, removed_at, attributes, image_data, image_id, created_by, updated_by, planted_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, '', $9, $10, $11, $12)
RETURNING id, created_at, updated_at, version`,
plant.GardenID, plant.SpeciesID, plant.Name, plant.Notes, plant.AcquiredAt,
plant.Status, plant.RemovedAt, jsonValue(plant.Attributes), plant.ImageID, nullableUserID(plant.CreatedBy), nullableUserID(plant.UpdatedBy), nullableUserID(plant.PlantedBy),
).Scan(&plant.ID, &plant.CreatedAt, &plant.UpdatedAt, &plant.Version)
if err != nil {
return storage.Plant{}, recordError(err)
}
return plant, nil
}
// Get returns a plant within its garden.
func (m PlantModel) Get(gardenID, id int) (storage.Plant, error) {
ctx, cancel := contextWithTimeout()
defer cancel()
plant, err := scanPlant(m.DB.QueryRowContext(ctx, `SELECT `+plantColumns+` FROM plants p LEFT JOIN users u ON u.id=p.planted_by LEFT JOIN images i ON i.id=p.image_id WHERE p.garden_id = $1 AND p.id = $2`, gardenID, id))
if err != nil {
return storage.Plant{}, recordError(err)
}
return plant, nil
}
// GetAllForGarden lists plants in a garden.
func (m PlantModel) GetAllForGarden(gardenID int) ([]storage.Plant, error) {
ctx, cancel := contextWithTimeout()
defer cancel()
rows, err := m.DB.QueryContext(ctx, `SELECT `+plantColumns+`
FROM plants p LEFT JOIN users u ON u.id=p.planted_by LEFT JOIN images i ON i.id=p.image_id WHERE p.garden_id = $1 ORDER BY p.name, p.id`, gardenID)
if err != nil {
return nil, err
}
defer rows.Close()
plants := []storage.Plant{}
for rows.Next() {
plant, err := scanPlant(rows)
if err != nil {
return nil, err
}
plants = append(plants, plant)
}
return plants, rows.Err()
}
// Update changes a plant using optimistic locking.
func (m PlantModel) Update(gardenID int, plant storage.Plant) (storage.Plant, error) {
ctx, cancel := contextWithTimeout()
defer cancel()
err := m.DB.QueryRowContext(ctx, `
UPDATE plants SET species_id = $1, name = $2, notes = $3, acquired_at = $4,
status = $5, removed_at = $6, attributes = $7, image_data = '', image_id = $8, updated_by = $9,
updated_at = CURRENT_TIMESTAMP, version = version + 1
WHERE garden_id = $10 AND id = $11 AND version = $12
RETURNING updated_at, version`,
plant.SpeciesID, plant.Name, plant.Notes, plant.AcquiredAt, plant.Status,
plant.RemovedAt, jsonValue(plant.Attributes), plant.ImageID, nullableUserID(plant.UpdatedBy), gardenID, plant.ID, plant.Version,
).Scan(&plant.UpdatedAt, &plant.Version)
if err != nil {
if err == sql.ErrNoRows {
return storage.Plant{}, storage.ErrEditConflict
}
return storage.Plant{}, recordError(err)
}
return plant, nil
}
// Delete removes a plant within its garden.
func (m PlantModel) Delete(gardenID, id int) error {
return deleteByGardenID(m.DB, `DELETE FROM plants WHERE garden_id = $1 AND id = $2`, gardenID, id)
}