105 lines
4.2 KiB
Go
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)
|
|
}
|