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