Revision history: change sets + revisions + revert — the undo substrate (#48) (#61)
Build image / build-and-push (push) Successful in 5s
Build image / build-and-push (push) Successful in 5s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #61.
This commit is contained in:
+54
-2
@@ -2,6 +2,8 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
@@ -151,6 +153,14 @@ func (s *Service) fillLoaded(ctx context.Context, actorID int64, o *domain.Garde
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// One record call with every plop, so a fill auto-scopes into ONE change set
|
||||
// with N revisions — undoing a fill is one click, not N.
|
||||
changes := make([]change, 0, len(created))
|
||||
for i := range created {
|
||||
changes = append(changes, changeCreate(domain.EntityPlanting, created[i].ID, &created[i]))
|
||||
}
|
||||
s.record(ctx, o.GardenID, actorID,
|
||||
fmt.Sprintf("Planted %d %s in %s", len(created), plant.Name, objectLabel(o)), changes...)
|
||||
for i := range created {
|
||||
created[i].DerivedCount = derivedCount(created[i].RadiusCM, spacing)
|
||||
}
|
||||
@@ -219,11 +229,53 @@ func (s *Service) FillNamedRegion(ctx context.Context, actorID, objectID int64,
|
||||
// non-plantable after it was planted must still be clearable (you can always
|
||||
// remove existing plops, only not add new ones).
|
||||
func (s *Service) ClearObject(ctx context.Context, actorID, objectID int64) (int, error) {
|
||||
if _, _, err := s.objectForRole(ctx, actorID, objectID, roleEditor); err != nil {
|
||||
o, g, err := s.objectForRole(ctx, actorID, objectID, roleEditor)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Snapshot the rows the bulk UPDATE is about to touch, since it reports only a
|
||||
// count — then clear exactly those ids. Clearing "every active plop" instead
|
||||
// would let a plop created between this read and the UPDATE be removed with no
|
||||
// revision recorded: cleared, with no way to undo it.
|
||||
before, err := s.store.ListActivePlantingsForObject(ctx, objectID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ids := make([]int64, 0, len(before))
|
||||
for i := range before {
|
||||
ids = append(ids, before[i].ID)
|
||||
}
|
||||
today := s.now().UTC().Format(dateLayout)
|
||||
return s.store.ClearObjectPlantings(ctx, objectID, today)
|
||||
n, err := s.store.ClearObjectPlantings(ctx, objectID, today, ids)
|
||||
if err != nil || n == 0 {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// From here the clear HAS happened. A failure to build the history entry must
|
||||
// not be reported as a failed clear — the caller would retry an operation that
|
||||
// already applied. Log the gap and report success, matching how record()
|
||||
// treats its own write failures.
|
||||
after, err := s.store.ListPlantingsForObject(ctx, objectID)
|
||||
if err != nil {
|
||||
slog.Error("service: clear succeeded but history could not be recorded",
|
||||
"error", err, "object", objectID, "cleared", n)
|
||||
return n, nil
|
||||
}
|
||||
afterByID := make(map[int64]*domain.Planting, len(after))
|
||||
for i := range after {
|
||||
afterByID[after[i].ID] = &after[i]
|
||||
}
|
||||
changes := make([]change, 0, len(before))
|
||||
for i := range before {
|
||||
b := before[i]
|
||||
a, ok := afterByID[b.ID]
|
||||
if !ok {
|
||||
continue // deleted outright between the two reads; nothing coherent to record
|
||||
}
|
||||
changes = append(changes, changeUpdate(domain.EntityPlanting, b.ID, &b, a))
|
||||
}
|
||||
s.record(ctx, g.ID, actorID, fmt.Sprintf("Cleared %s (%d plantings)", objectLabel(o), n), changes...)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// DescribeResult is a structured summary of a garden for prompting an agent.
|
||||
|
||||
Reference in New Issue
Block a user