@@ -0,0 +1,116 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"gardomatic.kleiax.de/internal/storage"
|
||||
)
|
||||
|
||||
// GardenModel stores gardens in PostgreSQL.
|
||||
// GardenModel implements storage.GardenModelInterface for PostgreSQL.
|
||||
type GardenModel struct{ DB *sql.DB }
|
||||
|
||||
// Insert creates a garden and its owner membership atomically.
|
||||
func (m GardenModel) Insert(garden storage.Garden, ownerID int) (storage.Garden, error) {
|
||||
ctx, cancel := contextWithTimeout()
|
||||
defer cancel()
|
||||
|
||||
tx, err := m.DB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return storage.Garden{}, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
query := `
|
||||
INSERT INTO gardens (name, description, image_data, image_id)
|
||||
VALUES ($1, $2, '', $3)
|
||||
RETURNING id, created_at, updated_at, version`
|
||||
err = tx.QueryRowContext(ctx, query, garden.Name, garden.Description, garden.ImageID).Scan(
|
||||
&garden.ID, &garden.CreatedAt, &garden.UpdatedAt, &garden.Version,
|
||||
)
|
||||
if err != nil {
|
||||
return storage.Garden{}, err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO garden_members (garden_id, user_id, role)
|
||||
VALUES ($1, $2, $3)`, garden.ID, ownerID, storage.GardenRoleOwner)
|
||||
if err != nil {
|
||||
return storage.Garden{}, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return storage.Garden{}, err
|
||||
}
|
||||
return garden, nil
|
||||
}
|
||||
|
||||
func scanGarden(s scanner) (storage.Garden, error) {
|
||||
var garden storage.Garden
|
||||
err := s.Scan(&garden.ID, &garden.Name, &garden.Description, &garden.ImageData, &garden.ImageID, &garden.CreatedAt, &garden.UpdatedAt, &garden.Version)
|
||||
return garden, err
|
||||
}
|
||||
|
||||
// Get returns a garden by ID.
|
||||
func (m GardenModel) Get(id int) (storage.Garden, error) {
|
||||
ctx, cancel := contextWithTimeout()
|
||||
defer cancel()
|
||||
garden, err := scanGarden(m.DB.QueryRowContext(ctx, `
|
||||
SELECT g.id, g.name, g.description, COALESCE('data:'||i.media_type||';base64,'||replace(encode(i.data,'base64'), E'\n', ''), ''), g.image_id, g.created_at, g.updated_at, g.version
|
||||
FROM gardens g LEFT JOIN images i ON i.id=g.image_id WHERE g.id = $1`, id))
|
||||
if err != nil {
|
||||
return storage.Garden{}, recordError(err)
|
||||
}
|
||||
return garden, nil
|
||||
}
|
||||
|
||||
// GetAllForUser lists gardens visible to a user with resolved permissions.
|
||||
func (m GardenModel) GetAllForUser(userID int) ([]storage.Garden, error) {
|
||||
ctx, cancel := contextWithTimeout()
|
||||
defer cancel()
|
||||
rows, err := m.DB.QueryContext(ctx, `
|
||||
SELECT g.id, g.name, g.description, COALESCE('data:'||i.media_type||';base64,'||replace(encode(i.data,'base64'), E'\n', ''), ''), g.image_id, g.created_at, g.updated_at, g.version, gm.role
|
||||
FROM gardens g
|
||||
INNER JOIN garden_members gm ON gm.garden_id = g.id
|
||||
LEFT JOIN images i ON i.id=g.image_id
|
||||
WHERE gm.user_id = $1
|
||||
ORDER BY g.name, g.id`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
gardens := []storage.Garden{}
|
||||
for rows.Next() {
|
||||
var garden storage.Garden
|
||||
err := rows.Scan(&garden.ID, &garden.Name, &garden.Description, &garden.ImageData, &garden.ImageID, &garden.CreatedAt, &garden.UpdatedAt, &garden.Version, &garden.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gardens = append(gardens, garden)
|
||||
}
|
||||
return gardens, rows.Err()
|
||||
}
|
||||
|
||||
// Update changes a garden using optimistic locking.
|
||||
func (m GardenModel) Update(garden storage.Garden) (storage.Garden, error) {
|
||||
ctx, cancel := contextWithTimeout()
|
||||
defer cancel()
|
||||
err := m.DB.QueryRowContext(ctx, `
|
||||
UPDATE gardens
|
||||
SET name = $1, description = $2, image_data = '', image_id = $3, updated_at = CURRENT_TIMESTAMP, version = version + 1
|
||||
WHERE id = $4 AND version = $5
|
||||
RETURNING updated_at, version`, garden.Name, garden.Description, garden.ImageID, garden.ID, garden.Version,
|
||||
).Scan(&garden.UpdatedAt, &garden.Version)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return storage.Garden{}, storage.ErrEditConflict
|
||||
}
|
||||
return storage.Garden{}, err
|
||||
}
|
||||
return garden, nil
|
||||
}
|
||||
|
||||
// Delete removes a garden and its dependent records.
|
||||
func (m GardenModel) Delete(id int) error {
|
||||
return deleteByID(m.DB, `DELETE FROM gardens WHERE id = $1`, id)
|
||||
}
|
||||
Reference in New Issue
Block a user