Address Gadfly review on #7: garden cap, dim validation, shared errors
Build image / build-and-push (push) Successful in 5s
Build image / build-and-push (push) Successful in 5s
Fixes from the PR #26 adversarial review (graded 18 real / 0 false positive). Correctness / security - maxGardenCM fixed to 10_000 (100 m), matching its comment — it was 100_000 cm (1 km), 10x too lax (5 models flagged this). - Dimension validation now rejects NaN/Inf (which slip past naive comparisons) and subnormal-tiny positives, via a finite [1cm, 100m] check. Name (200) and notes (10_000) are length-capped so untrusted input can't balloon storage. - Update version binding is `required,min=1`, so a negative/zero version is a 400, not a 409. Maintainability / performance - One unified writeServiceError (new errors.go) maps every auth + resource sentinel; writeResourceError removed. writeVersionConflict and parseIDParam moved to errors.go (shared, not in the gardens feature file). - Request structs share an embedded gardenFields (one toInput). - CreateGarden uses INSERT ... RETURNING (one round-trip). - ListGardensForOwner has a defensive LIMIT (pagination is post-v1). Tests: name/notes length, NaN/Inf/subnormal dims, dimension-at-cap valid, negative/zero version -> 400. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
This commit is contained in:
+12
-10
@@ -23,22 +23,24 @@ func scanGarden(s scanner) (*domain.Garden, error) {
|
||||
return &g, nil
|
||||
}
|
||||
|
||||
// maxGardensListed caps ListGardensForOwner as a defensive backstop against a
|
||||
// pathologically large result. At household scale a user has a handful of
|
||||
// gardens, so this is never hit; genuine pagination is post-v1 if ever needed.
|
||||
const maxGardensListed = 1000
|
||||
|
||||
// CreateGarden inserts a garden (owner_id, name, dimensions, unit, notes already
|
||||
// set and validated by the service) and returns the stored row.
|
||||
func (d *DB) CreateGarden(ctx context.Context, g *domain.Garden) (*domain.Garden, error) {
|
||||
res, err := d.sql.ExecContext(ctx,
|
||||
created, err := scanGarden(d.sql.QueryRowContext(ctx,
|
||||
`INSERT INTO gardens (owner_id, name, width_cm, height_cm, unit_pref, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
RETURNING `+gardenColumns,
|
||||
g.OwnerID, g.Name, g.WidthCM, g.HeightCM, g.UnitPref, g.Notes,
|
||||
)
|
||||
))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: insert garden: %w", err)
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: garden insert id: %w", err)
|
||||
}
|
||||
return d.GetGarden(ctx, id)
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// GetGarden returns the garden with the given id, or domain.ErrNotFound.
|
||||
@@ -59,8 +61,8 @@ func (d *DB) GetGarden(ctx context.Context, id int64) (*domain.Garden, error) {
|
||||
// added in #16.
|
||||
func (d *DB) ListGardensForOwner(ctx context.Context, ownerID int64) ([]domain.Garden, error) {
|
||||
rows, err := d.sql.QueryContext(ctx,
|
||||
`SELECT `+gardenColumns+` FROM gardens WHERE owner_id = ? ORDER BY created_at DESC, id DESC`,
|
||||
ownerID,
|
||||
`SELECT `+gardenColumns+` FROM gardens WHERE owner_id = ? ORDER BY created_at DESC, id DESC LIMIT ?`,
|
||||
ownerID, maxGardensListed,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: list gardens: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user