119 lines
4.7 KiB
Go
119 lines
4.7 KiB
Go
package postgres
|
|
|
|
import (
|
|
"database/sql"
|
|
|
|
"gardomatic.kleiax.de/internal/storage"
|
|
)
|
|
|
|
// LocationModel stores garden locations in PostgreSQL.
|
|
// LocationModel implements storage.LocationModelInterface for PostgreSQL and
|
|
// scopes hierarchical locations to their garden.
|
|
type LocationModel struct{ DB *sql.DB }
|
|
|
|
const locationColumns = `l.id, l.garden_id, l.parent_id, l.name, l.description, l.kind, l.area_sqm,
|
|
l.sun_exposure, l.soil_condition, l.soil_reaction, l.attributes, COALESCE('data:'||i.media_type||';base64,'||replace(encode(i.data,'base64'), E'\n', ''), ''), l.image_id, l.created_at, l.updated_at, l.version,
|
|
COALESCE(l.created_by, 0), COALESCE(l.updated_by, 0)`
|
|
|
|
func scanLocation(s scanner) (storage.Location, error) {
|
|
var location storage.Location
|
|
err := s.Scan(
|
|
&location.ID, &location.GardenID, &location.ParentID, &location.Name,
|
|
&location.Description, &location.Kind, &location.AreaSQM, &location.SunExposure, &location.SoilCondition, &location.SoilReaction,
|
|
&location.Attributes, &location.ImageData, &location.ImageID, &location.CreatedAt, &location.UpdatedAt, &location.Version, &location.CreatedBy, &location.UpdatedBy,
|
|
)
|
|
return location, err
|
|
}
|
|
|
|
// Insert creates a location in its garden.
|
|
func (m LocationModel) Insert(location storage.Location) (storage.Location, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
err := m.DB.QueryRowContext(ctx, `
|
|
INSERT INTO locations
|
|
(garden_id, parent_id, name, description, kind, area_sqm, sun_exposure, soil_condition, soil_reaction, attributes, image_data, image_id, created_by, updated_by)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, '', $11, $12, $13)
|
|
RETURNING id, created_at, updated_at, version`,
|
|
location.GardenID, location.ParentID, location.Name, location.Description,
|
|
location.Kind, location.AreaSQM, location.SunExposure, location.SoilCondition, location.SoilReaction, jsonValue(location.Attributes), location.ImageID, nullableUserID(location.CreatedBy), nullableUserID(location.UpdatedBy),
|
|
).Scan(&location.ID, &location.CreatedAt, &location.UpdatedAt, &location.Version)
|
|
if err != nil {
|
|
return storage.Location{}, err
|
|
}
|
|
return location, nil
|
|
}
|
|
|
|
// Get returns a location within its garden.
|
|
func (m LocationModel) Get(gardenID, id int) (storage.Location, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
location, err := scanLocation(m.DB.QueryRowContext(ctx, `SELECT `+locationColumns+` FROM locations l LEFT JOIN images i ON i.id=l.image_id WHERE l.garden_id = $1 AND l.id = $2`, gardenID, id))
|
|
if err != nil {
|
|
return storage.Location{}, recordError(err)
|
|
}
|
|
return location, nil
|
|
}
|
|
|
|
// GetAllForGarden lists locations in a garden.
|
|
func (m LocationModel) GetAllForGarden(gardenID int) ([]storage.Location, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
rows, err := m.DB.QueryContext(ctx, `SELECT `+locationColumns+`
|
|
FROM locations l LEFT JOIN images i ON i.id=l.image_id WHERE l.garden_id = $1 ORDER BY l.name, l.id`, gardenID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
locations := []storage.Location{}
|
|
for rows.Next() {
|
|
location, err := scanLocation(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
locations = append(locations, location)
|
|
}
|
|
return locations, rows.Err()
|
|
}
|
|
|
|
// Update changes a location using optimistic locking.
|
|
func (m LocationModel) Update(gardenID int, location storage.Location) (storage.Location, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
err := m.DB.QueryRowContext(ctx, `
|
|
UPDATE locations SET parent_id = $1, name = $2, description = $3, kind = $4,
|
|
area_sqm = $5, sun_exposure = $6, soil_condition = $7, soil_reaction = $8, attributes = $9, image_data = '', image_id = $10, updated_by = $11,
|
|
updated_at = CURRENT_TIMESTAMP, version = version + 1
|
|
WHERE garden_id = $12 AND id = $13 AND version = $14
|
|
RETURNING updated_at, version`,
|
|
location.ParentID, location.Name, location.Description, location.Kind,
|
|
location.AreaSQM, location.SunExposure, location.SoilCondition, location.SoilReaction, jsonValue(location.Attributes), location.ImageID, nullableUserID(location.UpdatedBy),
|
|
gardenID, location.ID, location.Version,
|
|
).Scan(&location.UpdatedAt, &location.Version)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return storage.Location{}, storage.ErrEditConflict
|
|
}
|
|
return storage.Location{}, err
|
|
}
|
|
return location, nil
|
|
}
|
|
|
|
// Delete removes a location within its garden.
|
|
func (m LocationModel) Delete(gardenID, id int) error {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
result, err := m.DB.ExecContext(ctx, `DELETE FROM locations WHERE garden_id = $1 AND id = $2`, gardenID, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rows == 0 {
|
|
return storage.ErrRecordNotFound
|
|
}
|
|
return nil
|
|
}
|