Address Gadfly review on #10: clearable color/props, column lists, dedup
Build image / build-and-push (push) Successful in 7s
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
This commit is contained in:
@@ -7,9 +7,11 @@ import (
|
||||
"gitea.stevedudenhoeffer.com/steve/pansy/internal/domain"
|
||||
)
|
||||
|
||||
// scanPlanting reads a plantings row in table-declared column order (id,
|
||||
// object_id, plant_id, x_cm, y_cm, radius_cm, count, label, planted_at,
|
||||
// removed_at, version, created_at, updated_at) — the order SELECT pl.* yields.
|
||||
// plantingColumns lists plantings columns in the order scanPlanting expects.
|
||||
// Used unqualified for direct selects; the /full read below qualifies with pl.
|
||||
const plantingColumns = `id, object_id, plant_id, x_cm, y_cm, radius_cm, count, label,
|
||||
planted_at, removed_at, version, created_at, updated_at`
|
||||
|
||||
func scanPlanting(s scanner) (*domain.Planting, error) {
|
||||
var p domain.Planting
|
||||
if err := s.Scan(
|
||||
@@ -27,10 +29,8 @@ func scanPlanting(s scanner) (*domain.Planting, error) {
|
||||
// non-nil slice (empty until plantings CRUD lands in #14). Plop CRUD itself is
|
||||
// #14; this is only the /full read side.
|
||||
func (d *DB) ListActivePlantingsForGarden(ctx context.Context, gardenID int64) ([]domain.Planting, error) {
|
||||
// pl.* returns the plantings columns in table-declared order, which matches
|
||||
// plantingColumns / scanPlanting.
|
||||
rows, err := d.sql.QueryContext(ctx,
|
||||
`SELECT pl.* FROM plantings pl
|
||||
`SELECT `+qualifyColumns("pl", plantingColumns)+` FROM plantings pl
|
||||
JOIN garden_objects o ON o.id = pl.object_id
|
||||
WHERE o.garden_id = ? AND pl.removed_at IS NULL
|
||||
ORDER BY pl.id`,
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"gitea.stevedudenhoeffer.com/steve/pansy/internal/domain"
|
||||
)
|
||||
|
||||
// scanPlant reads a plants row in table-declared column order (id, owner_id,
|
||||
// name, category, spacing_cm, color, icon, days_to_maturity, notes, version,
|
||||
// created_at, updated_at) — the order SELECT p.* yields.
|
||||
// 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(
|
||||
@@ -26,9 +27,8 @@ func scanPlant(s scanner) (*domain.Plant, error) {
|
||||
// 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) {
|
||||
// p.* returns the plants columns in table-declared order, matching scanPlant.
|
||||
rows, err := d.sql.QueryContext(ctx,
|
||||
`SELECT DISTINCT p.* FROM plants p
|
||||
`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
|
||||
|
||||
@@ -99,6 +99,17 @@ func pragmaName(p string) string {
|
||||
return strings.ToLower(strings.TrimSpace(p))
|
||||
}
|
||||
|
||||
// qualifyColumns prefixes each comma-separated column in cols with "alias." so a
|
||||
// shared column list can be used in a JOIN — e.g. qualifyColumns("pl", "id, x")
|
||||
// → "pl.id, pl.x". Whitespace/newlines in the list are trimmed.
|
||||
func qualifyColumns(alias, cols string) string {
|
||||
parts := strings.Split(cols, ",")
|
||||
for i, p := range parts {
|
||||
parts[i] = alias + "." + strings.TrimSpace(p)
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// Close closes the underlying database.
|
||||
func (d *DB) Close() error { return d.sql.Close() }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user