From 7a03640b88fb15ca5e8b7dc4b3c34f79f9474083 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 27 Oct 2026 15:36:11 +0100 Subject: [PATCH] fix: report the old translated content_html on API post events syncTranslations adds content_html to a translation that sends content, after snapshotPost read the old values, so the event saw content_html as newly submitted and reported Old: nil for a translation that existed. submittedTranslations now lists content_html whenever a non-blank content is sent, so the snapshot and the event cover the same fields. --- controllers/api/record_events.go | 18 ++++++++++++++++-- record_events_test.go | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/controllers/api/record_events.go b/controllers/api/record_events.go index f743db4..95568b1 100644 --- a/controllers/api/record_events.go +++ b/controllers/api/record_events.go @@ -2,9 +2,11 @@ package api import ( "context" + "fmt" "reflect" "sort" "strconv" + "strings" "time" "git.golem15.com/golem15/sm-journal-plugin/models" @@ -121,7 +123,11 @@ func postSavedEvent(ctx context.Context, db *gorm.DB, before *postSnapshot, post } // submittedTranslations lists, per locale, the translated fields a request -// body submits with a value, in translatedEventFields order. +// 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 +// content (classes.FormatTranslation): the snapshot before the write and the +// event after it then cover the same fields, whether or not the write has +// already added content_html to the body. func submittedTranslations(body map[string]any) map[string][]string { out := map[string][]string{} m, ok := body["translations"].(map[string]any) @@ -134,7 +140,8 @@ func submittedTranslations(body map[string]any) map[string][]string { continue } for _, field := range translatedEventFields { - if v, has := attrs[field]; has && v != nil { + v, has := attrs[field] + if (has && v != nil) || (field == "content_html" && derivesContentHTML(attrs)) { out[locale] = append(out[locale], field) } } @@ -142,6 +149,13 @@ func submittedTranslations(body map[string]any) map[string][]string { return out } +// derivesContentHTML reports whether a translation's attributes carry a +// content the write turns into content_html (a non-blank value). +func derivesContentHTML(attrs map[string]any) bool { + content, has := attrs["content"] + return has && content != nil && strings.TrimSpace(fmt.Sprint(content)) != "" +} + // derefValue is a pointer column's value, nil for a nil pointer. func derefValue[T any](p *T) any { if p == nil { diff --git a/record_events_test.go b/record_events_test.go index 8ff53bc..5c466b5 100644 --- a/record_events_test.go +++ b/record_events_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "sync" "testing" @@ -239,6 +240,24 @@ func TestAPIRecordEvents(t *testing.T) { t.Fatalf("PUT described URL = %q", got[0].url) } + // A translated content derives its content_html: the event reports the + // stored content_html as Old, not nil (WR-05). + env.expect(t, http.StatusOK, http.MethodPut, "/_journal/api/v1/posts/"+id, token, map[string]any{ + "translations": map[string]any{"pl": map[string]any{"content": "Stara treść"}}, + }) + l.reset() + env.expect(t, http.StatusOK, http.MethodPut, "/_journal/api/v1/posts/"+id, token, map[string]any{ + "translations": map[string]any{"pl": map[string]any{"content": "Nowa treść"}}, + }) + got = l.savedEvents() + if len(got) != 1 { + t.Fatalf("PUT content events = %+v, want one RecordSaved", got) + } + html, ok := findChange(got[0].ev.Changes, "content_html", "pl") + if old, _ := html.Old.(string); !ok || !strings.Contains(old, "Stara") || !strings.Contains(fmt.Sprint(html.New), "Nowa") { + t.Fatalf("pl content_html change = %+v (found %v); changes %+v", html, ok, got[0].ev.Changes) + } + // A write that fails after the row is saved rolls the row back and // fires nothing (WR-04). l.reset()