@@ -0,0 +1,104 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user