diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d2199b..084e625d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.23.4 + +- Fixed `autodate` fields not refreshing when calling `Save` multiple times on the same `Record` instance ([#6000](https://github.com/pocketbase/pocketbase/issues/6000)). + + ## v0.23.3 - Fixed Gzip middleware not applying when serving static files. diff --git a/core/field_autodate.go b/core/field_autodate.go index ad5fc066..ca29bed2 100644 --- a/core/field_autodate.go +++ b/core/field_autodate.go @@ -16,6 +16,9 @@ func init() { const FieldTypeAutodate = "autodate" +// used to keep track of the last set autodate value +const autodateLastKnownPrefix = internalCustomFieldKeyPrefix + "_last_autodate_" + var ( _ Field = (*AutodateField)(nil) _ SetterFinder = (*AutodateField)(nil) @@ -167,24 +170,46 @@ func (f *AutodateField) Intercept( actionFunc func() error, ) error { switch actionName { - case InterceptorActionCreate: - // ignore for custom date manually set with record.SetRaw() - if f.OnCreate && !f.hasBeenManuallyChanged(record) { - record.SetRaw(f.Name, types.NowDateTime()) + case InterceptorActionCreateExecute: + // ignore if a date different from the old one was manually set with SetRaw + if f.OnCreate && record.GetDateTime(f.Name).Equal(f.getLastKnownValue(record)) { + now := types.NowDateTime() + record.SetRaw(f.Name, now) + record.SetRaw(autodateLastKnownPrefix+f.Name, now) // eagerly set so that it can be renewed on resave after failure } - case InterceptorActionUpdate: - // ignore for custom date manually set with record.SetRaw() - if f.OnUpdate && !f.hasBeenManuallyChanged(record) { - record.SetRaw(f.Name, types.NowDateTime()) + + if err := actionFunc(); err != nil { + return err } + + record.SetRaw(autodateLastKnownPrefix+f.Name, record.GetRaw(f.Name)) + + return nil + case InterceptorActionUpdateExecute: + // ignore if a date different from the old one was manually set with SetRaw + if f.OnUpdate && record.GetDateTime(f.Name).Equal(f.getLastKnownValue(record)) { + now := types.NowDateTime() + record.SetRaw(f.Name, now) + record.SetRaw(autodateLastKnownPrefix+f.Name, now) // eagerly set so that it can be renewed on resave after failure + } + + if err := actionFunc(); err != nil { + return err + } + + record.SetRaw(autodateLastKnownPrefix+f.Name, record.GetRaw(f.Name)) + + return nil + default: + return actionFunc() + } +} + +func (f *AutodateField) getLastKnownValue(record *Record) types.DateTime { + v := record.GetDateTime(autodateLastKnownPrefix + f.Name) + if !v.IsZero() { + return v } - return actionFunc() -} - -func (f *AutodateField) hasBeenManuallyChanged(record *Record) bool { - vNew, _ := record.GetRaw(f.Name).(types.DateTime) - vOld, _ := record.Original().GetRaw(f.Name).(types.DateTime) - - return vNew.String() != vOld.String() + return record.Original().GetDateTime(f.Name) } diff --git a/core/field_autodate_test.go b/core/field_autodate_test.go index 94fe0ec9..689dc198 100644 --- a/core/field_autodate_test.go +++ b/core/field_autodate_test.go @@ -2,6 +2,7 @@ package core_test import ( "context" + "errors" "fmt" "strings" "testing" @@ -9,6 +10,7 @@ import ( "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/hook" "github.com/pocketbase/pocketbase/tools/types" ) @@ -226,6 +228,13 @@ func TestAutodateFieldFindSetter(t *testing.T) { }) } +func cutMilliseconds(datetime string) string { + if len(datetime) > 19 { + return datetime[:19] + } + return datetime +} + func TestAutodateFieldIntercept(t *testing.T) { app, _ := tests.NewTestApp() defer app.Cleanup() @@ -341,9 +350,92 @@ func TestAutodateFieldIntercept(t *testing.T) { } } -func cutMilliseconds(datetime string) string { - if len(datetime) > 19 { - return datetime[:19] +func TestAutodateRecordResave(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := app.FindCollectionByNameOrId("demo2") + if err != nil { + t.Fatal(err) + } + + record, err := app.FindRecordById(collection, "llvuca81nly1qls") + if err != nil { + t.Fatal(err) + } + + lastUpdated := record.GetDateTime("updated") + + // save with autogenerated date + err = app.Save(record) + if err != nil { + t.Fatal(err) + } + + newUpdated := record.GetDateTime("updated") + if newUpdated.Equal(lastUpdated) { + t.Fatalf("[0] Expected updated to change, got %v", newUpdated) + } + lastUpdated = newUpdated + + // save with custom date + manualUpdated := lastUpdated.Add(-1 * time.Minute) + record.SetRaw("updated", manualUpdated) + err = app.Save(record) + if err != nil { + t.Fatal(err) + } + + newUpdated = record.GetDateTime("updated") + if !newUpdated.Equal(manualUpdated) { + t.Fatalf("[1] Expected updated to be the manual set date %v, got %v", manualUpdated, newUpdated) + } + lastUpdated = newUpdated + + // save again with autogenerated date + err = app.Save(record) + if err != nil { + t.Fatal(err) + } + + newUpdated = record.GetDateTime("updated") + if newUpdated.Equal(lastUpdated) { + t.Fatalf("[2] Expected updated to change, got %v", newUpdated) + } + lastUpdated = newUpdated + + // simulate save failure + app.OnRecordUpdateExecute(collection.Id).Bind(&hook.Handler[*core.RecordEvent]{ + Id: "test_failure", + Func: func(*core.RecordEvent) error { + return errors.New("test") + }, + Priority: 9999999999, // as latest as possible + }) + + // save again with autogenerated date (should fail) + err = app.Save(record) + if err == nil { + t.Fatal("Expected save failure") + } + + // updated should still be set even after save failure + newUpdated = record.GetDateTime("updated") + if newUpdated.Equal(lastUpdated) { + t.Fatalf("[3] Expected updated to change, got %v", newUpdated) + } + lastUpdated = newUpdated + + // cleanup the error and resave again + app.OnRecordUpdateExecute(collection.Id).Unbind("test_failure") + + err = app.Save(record) + if err != nil { + t.Fatal(err) + } + + newUpdated = record.GetDateTime("updated") + if newUpdated.Equal(lastUpdated) { + t.Fatalf("[4] Expected updated to change, got %v", newUpdated) } - return datetime }