Copy a garden (deep duplicate) from the gardens list (#46)
Build image / build-and-push (push) Successful in 16s

Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #46.
This commit is contained in:
2026-07-21 03:58:39 +00:00
committed by steve
parent e74fb308c1
commit e22bdd6cab
15 changed files with 752 additions and 52 deletions
+31 -13
View File
@@ -31,19 +31,30 @@ func scanObject(s scanner) (*domain.GardenObject, error) {
return &o, nil
}
// objectInsert inserts one garden_objects row and returns it. It is shared by
// CreateObject and CopyGarden's in-transaction copy, with objectInsertArgs
// supplying its parameters, so a column added to the table can't be wired into
// one path and silently dropped from the other.
const objectInsert = `INSERT INTO garden_objects
(garden_id, kind, name, shape, points, x_cm, y_cm, width_cm, height_cm,
rotation_deg, z_index, plantable, color, props, grid_size_cm, snap_to_grid, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING ` + objectColumns
// objectInsertArgs builds objectInsert's parameters, placing o in gardenID (which
// is o's own garden on create, and the new garden when copying).
func objectInsertArgs(gardenID int64, o *domain.GardenObject) []any {
return []any{
gardenID, o.Kind, o.Name, o.Shape, o.Points, o.XCM, o.YCM, o.WidthCM, o.HeightCM,
o.RotationDeg, o.ZIndex, boolToInt(o.Plantable), o.Color, o.Props,
o.GridSizeCM, boolToInt(o.SnapToGrid), o.Notes,
}
}
// CreateObject inserts a garden object (fields already validated by the service)
// and returns the stored row.
func (d *DB) CreateObject(ctx context.Context, o *domain.GardenObject) (*domain.GardenObject, error) {
created, err := scanObject(d.sql.QueryRowContext(ctx,
`INSERT INTO garden_objects
(garden_id, kind, name, shape, points, x_cm, y_cm, width_cm, height_cm,
rotation_deg, z_index, plantable, color, props, grid_size_cm, snap_to_grid, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING `+objectColumns,
o.GardenID, o.Kind, o.Name, o.Shape, o.Points, o.XCM, o.YCM, o.WidthCM, o.HeightCM,
o.RotationDeg, o.ZIndex, boolToInt(o.Plantable), o.Color, o.Props,
o.GridSizeCM, boolToInt(o.SnapToGrid), o.Notes,
))
created, err := scanObject(d.sql.QueryRowContext(ctx, objectInsert, objectInsertArgs(o.GardenID, o)...))
if err != nil {
return nil, fmt.Errorf("store: insert object: %w", err)
}
@@ -66,10 +77,17 @@ func (d *DB) GetObject(ctx context.Context, id int64) (*domain.GardenObject, err
// ListObjectsForGarden returns a garden's objects, bottom-to-top (z_index then
// id). Always a non-nil slice.
func (d *DB) ListObjectsForGarden(ctx context.Context, gardenID int64) ([]domain.GardenObject, error) {
rows, err := d.sql.QueryContext(ctx,
return queryObjects(ctx, d.sql,
`SELECT `+objectColumns+` FROM garden_objects WHERE garden_id = ? ORDER BY z_index, id`,
gardenID,
)
gardenID)
}
// queryObjects runs an object query and scans every row. The result set is fully
// drained and the cursor closed before returning, so a caller inside a
// transaction may write on the same connection afterwards. Always a non-nil
// slice.
func queryObjects(ctx context.Context, q queryer, query string, args ...any) ([]domain.GardenObject, error) {
rows, err := q.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("store: list objects: %w", err)
}