feat: revision-check the upsert, MCP and admin writes
- POST /materials locks the matched row and answers 409 to a stale revision when it updates; a create ignores revision - bm_manage_save_material and bm_manage_update_material take revision - /bmadmin saves bump the revision on content changes (not checked) - document revisions and the 409 in the README
This commit is contained in:
@@ -549,7 +549,11 @@ func writeManaged(w http.ResponseWriter, r *http.Request, db *gorm.DB, id uint,
|
||||
// updates the one with the same title and course, so sending a document
|
||||
// twice changes nothing. body, source and url are replaced as sent; see
|
||||
// materialDraft.apply for published_at and tags. A renamed material is a
|
||||
// new one here: PUT /materials/{id} renames.
|
||||
// new one here: PUT /materials/{id} renames. revision is checked only when
|
||||
// the upsert updates an existing material, on its row locked for the write:
|
||||
// a stale one is a 409 with the current version (writeConflict) and nothing
|
||||
// is written. A create ignores it and starts at revision 1. The revision
|
||||
// goes up only when the content changed (models.ReviseMaterial).
|
||||
func (h *Handlers) UpsertMaterial(w http.ResponseWriter, r *http.Request) {
|
||||
callerID, ok := h.caller(w, r)
|
||||
if !ok {
|
||||
@@ -564,19 +568,37 @@ func (h *Handlers) UpsertMaterial(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
var m models.Material
|
||||
created := false
|
||||
err := lagoon.Transaction(r.Context(), db, func(_ context.Context, tx *gorm.DB) error {
|
||||
res := d.identity(tx).Order("id").Limit(1).Find(&m)
|
||||
created, conflict := false, false
|
||||
err := lagoon.Transaction(r.Context(), db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
res := d.identity(tx.Clauses(clause.Locking{Strength: "UPDATE"})).Order("id").Limit(1).Find(&m)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
created = res.RowsAffected == 0
|
||||
return d.apply(tx, &m, created, h.now(), callerID)
|
||||
if created {
|
||||
return d.apply(tx, &m, true, h.now(), callerID)
|
||||
}
|
||||
if staleRevision(d.in.Revision, m.Revision) {
|
||||
conflict = true
|
||||
return nil
|
||||
}
|
||||
before, err := models.MaterialContent(tx, m.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.apply(tx, &m, false, h.now(), callerID); err != nil {
|
||||
return err
|
||||
}
|
||||
return h.revise(ctx, tx, m.ID, before)
|
||||
})
|
||||
if err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
if conflict {
|
||||
writeConflict(w, r, db, m.ID)
|
||||
return
|
||||
}
|
||||
h.MaterialIndex.Sync(r.Context(), db, m.ID)
|
||||
status := http.StatusOK
|
||||
if created {
|
||||
|
||||
Reference in New Issue
Block a user