Files
sm-bm-plugin/controllers/api/material_revision.go
Jakub Zych 2a47186a7a feat: announce material revisions on materials:{id}
- every revision bump (API writes and /bmadmin saves) enqueues
  updated.jz.bm.material with {material_id, revision} in a savepoint
- the materials namespace authorizer allows a subscribe only to a
  visible material the user may manage
- api.ManagesMaterials and api.CanWatchMaterial
2026-11-09 01:12:49 +01:00

67 lines
2.4 KiB
Go

package api
import (
"context"
"net/http"
"git.golem15.com/golem15/summercms/modules/wire"
"git.golem15.com/jakub/sm-bm-plugin/models"
"gorm.io/gorm"
)
// A write of a material's content may carry the revision the writer last
// read (D-01, D-04). When it does and the stored revision differs, the write
// is refused with 409 and nothing is written; when it does not, the write
// overwrites as before, so callers that never send a revision keep working.
// The compare and the write run on the row locked FOR UPDATE, so two writers
// holding the same revision cannot both succeed.
// conflictMessage is the message of a 409 answer to a stale revision.
const conflictMessage = "The material was changed since the revision you sent."
// staleRevision reports whether a write that sent revision sent must be
// refused: true when it sent one and it is not the stored one.
func staleRevision(sent *uint, stored uint) bool {
return sent != nil && *sent != stored
}
// revise bumps the revision of material id inside tx when its content
// differs from before (models.ReviseMaterial). Every content writer of the
// API calls it after its write. A bump also enqueues the live heads-up on
// tx (D-20), published only when the write commits; its failure never fails
// the write.
func (h *Handlers) revise(ctx context.Context, tx *gorm.DB, id uint, before string) error {
rev, bumped, err := models.ReviseMaterial(tx.WithContext(ctx), id, before)
if err != nil {
return err
}
if bumped {
h.Revisions.Revised(ctx, tx, id, rev)
}
return nil
}
// writeConflict answers 409 to a write that sent a stale revision, with the
// current revision, its updated_at and the material as the management API
// shows it, so the writer can diff or pull. Under MCP the body is left out,
// as in every other MCP answer.
func writeConflict(w http.ResponseWriter, r *http.Request, db *gorm.DB, id uint) {
var cur models.Material
if err := preloadMaterial(db).First(&cur, id).Error; err != nil {
writeOpaque500(w)
return
}
people, err := loadMaterialPeople(db, cur)
if err != nil {
writeOpaque500(w)
return
}
wire.WriteJSON(w, http.StatusConflict, map[string]any{
"error": true,
"message": conflictMessage,
"revision": cur.Revision,
"updated_at": models.FormatDateTime(cur.UpdatedAt),
"data": managedMaterialView(newManagedMaterialJSON(cur, people), viaMCP(r), false),
})
}