fix: fire RecordSaved for tags the post API creates by name

journal-tag is a content type, but tags created on the fly from names in a
post write fired no event, so SEO and sitemap subscribers only heard of them
from a later bulk job. Each created tag now fires a RecordSaved with Created
inside the post write's transaction.
This commit is contained in:
2026-10-27 15:37:02 +01:00
parent 7a03640b88
commit a6fc7cd954
5 changed files with 76 additions and 19 deletions

View File

@@ -227,13 +227,17 @@ func Store(app *backpack.App) http.HandlerFunc {
if err := tx.Create(post).Error; err != nil {
return err
}
if err := applyPostAssocs(ctx, tx, post, body); err != nil {
tags, err := applyPostAssocs(ctx, tx, post, body)
if err != nil {
return err
}
if err := loadPostAssocs(tx, post); err != nil {
return err
}
cabana.EmitRecordSaved(ctx, app, tx, postSavedEvent(ctx, tx, nil, post, body))
for _, tag := range tags {
cabana.EmitRecordSaved(ctx, app, tx, tagSavedEvent(ctx, &tag))
}
return nil
})
if err != nil {
@@ -291,13 +295,17 @@ func Update(app *backpack.App) http.HandlerFunc {
if err := tx.Save(post).Error; err != nil {
return err
}
if err := applyPostAssocs(ctx, tx, post, body); err != nil {
tags, err := applyPostAssocs(ctx, tx, post, body)
if err != nil {
return err
}
if err := loadPostAssocs(tx, post); err != nil {
return err
}
cabana.EmitRecordSaved(ctx, app, tx, postSavedEvent(ctx, tx, before, post, body))
for _, tag := range tags {
cabana.EmitRecordSaved(ctx, app, tx, tagSavedEvent(ctx, &tag))
}
return nil
})
if err != nil {

View File

@@ -695,54 +695,59 @@ func asUintSlice(raw any) ([]uint, bool) {
}
}
func applyPostAssocs(ctx context.Context, db *gorm.DB, post *models.Post, body map[string]any) error {
// applyPostAssocs writes the post's categories, tags and translations the
// body submits. created are the tags the write created by name, for their
// record events.
func applyPostAssocs(ctx context.Context, db *gorm.DB, post *models.Post, body map[string]any) (created []models.Tag, err error) {
if _, has := body["categories"]; has {
ids, _ := asUintSlice(body["categories"])
if err := db.Where("post_id = ?", post.ID).Delete(&models.PostCategory{}).Error; err != nil {
return err
return nil, err
}
for _, id := range ids {
if err := db.Create(&models.PostCategory{PostID: post.ID, CategoryID: id}).Error; err != nil {
return err
return nil, err
}
}
}
if _, has := body["tags"]; has {
ids, err := resolveTagIDs(db, body["tags"])
var ids []uint
ids, created, err = resolveTagIDs(db, body["tags"])
if err != nil {
return err
return nil, err
}
if err := db.Where("post_id = ?", post.ID).Delete(&models.PostTag{}).Error; err != nil {
return err
return nil, err
}
for _, id := range ids {
if err := db.Create(&models.PostTag{PostID: post.ID, TagID: id}).Error; err != nil {
return err
return nil, err
}
}
}
if raw, has := body["translations"]; has {
if err := syncTranslations(ctx, db, post, raw); err != nil {
return err
return nil, err
}
}
return nil
return created, nil
}
func resolveTagIDs(db *gorm.DB, raw any) ([]uint, error) {
// resolveTagIDs turns the submitted tags (ids or names) into tag ids; a name
// no tag has yet creates the tag, returned in created.
func resolveTagIDs(db *gorm.DB, raw any) (ids []uint, created []models.Tag, err error) {
items, ok := raw.([]any)
if !ok {
if raw == nil {
return nil, nil
return nil, nil, nil
}
return nil, fmt.Errorf("tags must be an array")
return nil, nil, fmt.Errorf("tags must be an array")
}
var ids []uint
for _, item := range items {
if id, ok := asUint(item); ok {
var n int64
if err := db.Model(&models.Tag{}).Where("id = ?", id).Count(&n).Error; err != nil {
return nil, err
return nil, nil, err
}
if n > 0 {
ids = append(ids, id)
@@ -765,11 +770,12 @@ func resolveTagIDs(db *gorm.DB, raw any) ([]uint, error) {
}
tag = models.Tag{Name: name, Slug: slug}
if err := db.Create(&tag).Error; err != nil {
return nil, err
return nil, nil, err
}
ids = append(ids, tag.ID)
created = append(created, tag)
}
return ids, nil
return ids, created, nil
}
func syncTranslations(ctx context.Context, db *gorm.DB, post *models.Post, raw any) error {

View File

@@ -122,6 +122,22 @@ func postSavedEvent(ctx context.Context, db *gorm.DB, before *postSnapshot, post
}
}
// tagSavedEvent is the RecordSaved of a tag a post write created by name
// (journal-tag is a content type, D-102): Created, with its name and slug.
func tagSavedEvent(ctx context.Context, tag *models.Tag) pact.RecordSaved {
locale, _ := towel.Locale(ctx)
return pact.RecordSaved{
Model: tag.MorphName(),
ID: strconv.FormatUint(uint64(tag.ID), 10),
Locale: locale,
Created: true,
Changes: []pact.RecordChange{
{Attribute: "name", New: tag.Name},
{Attribute: "slug", New: tag.Slug},
},
}
}
// submittedTranslations lists, per locale, the translated fields a request
// body submits with a value, in translatedEventFields order. content_html is
// listed whenever a non-blank content is, as the write derives it from the