Build image / build-and-push (push) Successful in 7s
Fixes from the PR #28 adversarial review (considered; not graded). Correctness / API - PATCH /objects/:id can now clear nullable color/props back to NULL: the request takes them as json.RawMessage, and ObjectPatch carries an explicit Set flag so an explicit `null` (clear) is distinguished from an absent field (unchanged) — the strongest cross-model finding (6 hits). New test. Maintainability - store/plantings.go + plants.go use explicit qualified column lists (qualifyColumns helper) instead of SELECT *, matching gardens/objects and surviving a future column add. - Consolidated objectKinds + plantableByDefault into one kind→traits map. - objectForRole factors the fetch-then-authorize shared by UpdateObject and DeleteObject; dropped the redundant kind check in CreateObject (finalizeObject is the single validation point). - Request→service mapping via toInput()/toPatch() methods (matches gardens). - Renamed handler gardenFull → getGardenFull (verbNoun); test helper decodeGarden → decodeMap; generic bind-error messages. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
56 lines
1.6 KiB
Go
56 lines
1.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/pansy/internal/domain"
|
|
)
|
|
|
|
// plantColumns lists plants columns in the order scanPlant expects.
|
|
const plantColumns = `id, owner_id, name, category, spacing_cm, color, icon,
|
|
days_to_maturity, notes, version, created_at, updated_at`
|
|
|
|
func scanPlant(s scanner) (*domain.Plant, error) {
|
|
var p domain.Plant
|
|
if err := s.Scan(
|
|
&p.ID, &p.OwnerID, &p.Name, &p.Category, &p.SpacingCM, &p.Color, &p.Icon,
|
|
&p.DaysToMaturity, &p.Notes, &p.Version, &p.CreatedAt, &p.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
// ListReferencedPlants returns the distinct plants used by a garden's active
|
|
// plantings — the catalog subset the editor needs to render them. Always a
|
|
// non-nil slice (empty until plantings exist, #14). Full plant CRUD/seeding is
|
|
// #12; this is only the /full read side.
|
|
func (d *DB) ListReferencedPlants(ctx context.Context, gardenID int64) ([]domain.Plant, error) {
|
|
rows, err := d.sql.QueryContext(ctx,
|
|
`SELECT DISTINCT `+qualifyColumns("p", plantColumns)+` FROM plants p
|
|
JOIN plantings pl ON pl.plant_id = p.id
|
|
JOIN garden_objects o ON o.id = pl.object_id
|
|
WHERE o.garden_id = ? AND pl.removed_at IS NULL
|
|
ORDER BY p.id`,
|
|
gardenID,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("store: list referenced plants: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
plants := []domain.Plant{}
|
|
for rows.Next() {
|
|
p, err := scanPlant(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("store: scan plant: %w", err)
|
|
}
|
|
plants = append(plants, *p)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("store: iterate plants: %w", err)
|
|
}
|
|
return plants, nil
|
|
}
|