fix: write API posts in one transaction and fire RecordSaved after its commit
POST and PUT /_journal/api/v1/posts saved the row, then the categories, tags and translations, and fired RecordSaved only when all succeeded. A failure part-way left a committed row (possibly a new slug) with no event, so subscribers such as an SEO plugin never created the redirect. The write now runs in one lagoon transaction with the event emitted inside it.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"html"
|
||||
"net/http"
|
||||
@@ -220,20 +221,26 @@ func Store(app *backpack.App) http.HandlerFunc {
|
||||
writeAPIError(w, http.StatusForbidden, errPublishPermission)
|
||||
return
|
||||
}
|
||||
if err := db.WithContext(r.Context()).Create(post).Error; err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, "Internal server error")
|
||||
return
|
||||
}
|
||||
if err := applyPostAssocs(r.Context(), db.WithContext(r.Context()), post, body); err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, "Internal server error")
|
||||
return
|
||||
}
|
||||
if err := loadPostAssocs(db.WithContext(r.Context()), post); err != nil {
|
||||
// D-92/D-102: the row, its associations and translations commit
|
||||
// together, and RecordSaved fires only once they have.
|
||||
err = lagoon.Transaction(r.Context(), db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
if err := tx.Create(post).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := applyPostAssocs(ctx, tx, post, body); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := loadPostAssocs(tx, post); err != nil {
|
||||
return err
|
||||
}
|
||||
cabana.EmitRecordSaved(ctx, app, tx, postSavedEvent(ctx, tx, nil, post, body))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, "Internal server error")
|
||||
return
|
||||
}
|
||||
purgeSiteCache(r, app, post)
|
||||
cabana.EmitRecordSaved(r.Context(), app, nil, postSavedEvent(r.Context(), db.WithContext(r.Context()), nil, post, body))
|
||||
wire.WriteJSON(w, http.StatusCreated, map[string]any{"data": serializePost(post, db.WithContext(r.Context()))})
|
||||
}
|
||||
}
|
||||
@@ -278,20 +285,26 @@ func Update(app *backpack.App) http.HandlerFunc {
|
||||
writeAPIError(w, http.StatusForbidden, errPublishPermission)
|
||||
return
|
||||
}
|
||||
if err := db.WithContext(r.Context()).Save(post).Error; err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, "Internal server error")
|
||||
return
|
||||
}
|
||||
if err := applyPostAssocs(r.Context(), db.WithContext(r.Context()), post, body); err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, "Internal server error")
|
||||
return
|
||||
}
|
||||
if err := loadPostAssocs(db.WithContext(r.Context()), post); err != nil {
|
||||
// D-92/D-102: a slug change commits with its translations and
|
||||
// associations, or not at all, and RecordSaved follows the commit.
|
||||
err = lagoon.Transaction(r.Context(), db, func(ctx context.Context, tx *gorm.DB) error {
|
||||
if err := tx.Save(post).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := applyPostAssocs(ctx, tx, post, body); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := loadPostAssocs(tx, post); err != nil {
|
||||
return err
|
||||
}
|
||||
cabana.EmitRecordSaved(ctx, app, tx, postSavedEvent(ctx, tx, before, post, body))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, "Internal server error")
|
||||
return
|
||||
}
|
||||
purgeSiteCache(r, app, post)
|
||||
cabana.EmitRecordSaved(r.Context(), app, nil, postSavedEvent(r.Context(), db.WithContext(r.Context()), before, post, body))
|
||||
wire.WriteJSON(w, http.StatusOK, map[string]any{"data": serializePost(post, db.WithContext(r.Context()))})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +66,8 @@ func snapshotPost(ctx context.Context, db *gorm.DB, post *models.Post, body map[
|
||||
}
|
||||
|
||||
// postSavedEvent is the RecordSaved of an API write, which the handler
|
||||
// fires with cabana.EmitRecordSaved once the write is committed (the API
|
||||
// writes run outside a transaction, so at once). before is nil on create:
|
||||
// fires with cabana.EmitRecordSaved inside the write's transaction, so it
|
||||
// reaches subscribers once the write commits. before is nil on create:
|
||||
// every column with a value and every submitted translation is then
|
||||
// reported with a nil Old. Controller is "": the write did not go through an
|
||||
// admin form.
|
||||
|
||||
Reference in New Issue
Block a user