- add jz_bm_materials.revision (default 1), read-only for GORM
- bump it only on content changes (models.ReviseMaterial)
- PUT /materials/{id} locks the row and answers 409 to a stale revision
- management answers carry revision and updated_at, the student detail
carries revision
59 lines
2.1 KiB
Go
59 lines
2.1 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.
|
|
func (h *Handlers) revise(ctx context.Context, tx *gorm.DB, id uint, before string) error {
|
|
_, _, err := models.ReviseMaterial(tx.WithContext(ctx), id, before)
|
|
return err
|
|
}
|
|
|
|
// 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),
|
|
})
|
|
}
|