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:
@@ -0,0 +1,80 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"gitea.stevedudenhoeffer.com/steve/pansy/internal/domain"
|
||||
)
|
||||
|
||||
// Change history and undo (#48). Two endpoints: read a garden's change sets, and
|
||||
// revert one. Both encode domain types directly, like the rest of the package —
|
||||
// domain.ChangeSet already carries the actor name, revert linkage and per-op
|
||||
// counts the history list renders, and its Revisions field is omitempty so the
|
||||
// JSON snapshots never ride along on a list response.
|
||||
|
||||
// historyResponse is the body of GET /gardens/:id/history. hasMore lets the
|
||||
// client page without a separate count query over a table that only grows.
|
||||
type historyResponse struct {
|
||||
ChangeSets []domain.ChangeSet `json:"changeSets"`
|
||||
HasMore bool `json:"hasMore"`
|
||||
}
|
||||
|
||||
// revertResponse is the body of POST /change-sets/:id/revert on every path.
|
||||
// Carrying both fields regardless is what lets the UI say "2 of 3 changes undone;
|
||||
// the north bed was edited since and was left alone" instead of a generic
|
||||
// failure — a partial revert really did change things, and pretending otherwise
|
||||
// would be a lie. ChangeSet is null when nothing needed reverting.
|
||||
type revertResponse struct {
|
||||
ChangeSet *domain.ChangeSet `json:"changeSet"`
|
||||
Conflicts []domain.RevertConflict `json:"conflicts"`
|
||||
}
|
||||
|
||||
func (h *handlers) getGardenHistory(c *gin.Context) {
|
||||
gardenID, ok := parseIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// 0 means "the service's default"; it clamps the upper bound too.
|
||||
limit := intQuery(c, "limit", 0)
|
||||
offset := intQuery(c, "offset", 0)
|
||||
|
||||
sets, hasMore, err := h.svc.GardenHistory(c.Request.Context(), mustActor(c).ID, gardenID, limit, offset)
|
||||
if err != nil {
|
||||
writeServiceError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, historyResponse{ChangeSets: sets, HasMore: hasMore})
|
||||
}
|
||||
|
||||
func (h *handlers) revertChangeSet(c *gin.Context) {
|
||||
id, ok := parseIDParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// A revert through the REST API is a person clicking undo. The agent reverts
|
||||
// its own work through the service directly and stamps SourceAgent, so the
|
||||
// history badge can tell the two apart.
|
||||
cs, conflicts, err := h.svc.RevertChangeSet(c.Request.Context(), mustActor(c).ID, id, domain.SourceUI)
|
||||
if err != nil {
|
||||
writeServiceError(c, err)
|
||||
return
|
||||
}
|
||||
if conflicts == nil {
|
||||
conflicts = []domain.RevertConflict{}
|
||||
}
|
||||
body := revertResponse{ChangeSet: cs, Conflicts: conflicts}
|
||||
switch {
|
||||
case len(conflicts) > 0:
|
||||
// 409 even when part of the revert applied: something the caller asked for
|
||||
// did not happen, and the body says exactly what.
|
||||
c.JSON(http.StatusConflict, body)
|
||||
case cs == nil:
|
||||
// Every revision resolved to a no-op (already undone by hand, say). Nothing
|
||||
// was created, so 200 rather than a 201 pointing at nothing.
|
||||
c.JSON(http.StatusOK, body)
|
||||
default:
|
||||
c.JSON(http.StatusCreated, body)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user