diff --git a/apis/admin.go b/apis/admin.go index 2177d2c3..3ce305f7 100644 --- a/apis/admin.go +++ b/apis/admin.go @@ -166,7 +166,7 @@ func (api *adminApi) create(c echo.Context) error { // load request if err := c.Bind(form); err != nil { - return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) } event := &core.AdminCreateEvent{ @@ -174,20 +174,24 @@ func (api *adminApi) create(c echo.Context) error { Admin: admin, } - handlerErr := api.app.OnAdminBeforeCreateRequest().Trigger(event, func(e *core.AdminCreateEvent) error { - // create the admin - if err := form.Submit(); err != nil { - return rest.NewBadRequestError("Failed to create admin.", err) - } + // create the admin + submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + return api.app.OnAdminBeforeCreateRequest().Trigger(event, func(e *core.AdminCreateEvent) error { + if err := next(); err != nil { + return rest.NewBadRequestError("Failed to create admin.", err) + } - return e.HttpContext.JSON(http.StatusOK, e.Admin) + return e.HttpContext.JSON(http.StatusOK, e.Admin) + }) + } }) - if handlerErr == nil { + if submitErr == nil { api.app.OnAdminAfterCreateRequest().Trigger(event) } - return handlerErr + return submitErr } func (api *adminApi) update(c echo.Context) error { @@ -205,7 +209,7 @@ func (api *adminApi) update(c echo.Context) error { // load request if err := c.Bind(form); err != nil { - return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) } event := &core.AdminUpdateEvent{ @@ -213,20 +217,24 @@ func (api *adminApi) update(c echo.Context) error { Admin: admin, } - handlerErr := api.app.OnAdminBeforeUpdateRequest().Trigger(event, func(e *core.AdminUpdateEvent) error { - // update the admin - if err := form.Submit(); err != nil { - return rest.NewBadRequestError("Failed to update admin.", err) - } + // update the admin + submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + return api.app.OnAdminBeforeUpdateRequest().Trigger(event, func(e *core.AdminUpdateEvent) error { + if err := next(); err != nil { + return rest.NewBadRequestError("Failed to update admin.", err) + } - return e.HttpContext.JSON(http.StatusOK, e.Admin) + return e.HttpContext.JSON(http.StatusOK, e.Admin) + }) + } }) - if handlerErr == nil { + if submitErr == nil { api.app.OnAdminAfterUpdateRequest().Trigger(event) } - return handlerErr + return submitErr } func (api *adminApi) delete(c echo.Context) error { diff --git a/apis/admin_test.go b/apis/admin_test.go index 8b19c757..7a1c7d08 100644 --- a/apis/admin_test.go +++ b/apis/admin_test.go @@ -507,9 +507,6 @@ func TestAdminCreate(t *testing.T) { }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."},"password":{"code":"validation_required","message":"Cannot be blank."}}`}, - ExpectedEvents: map[string]int{ - "OnAdminBeforeCreateRequest": 1, - }, }, { Name: "authorized as admin + invalid data format", @@ -532,9 +529,6 @@ func TestAdminCreate(t *testing.T) { }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{"avatar":{"code":"validation_max_less_equal_than_required","message":"Must be no greater than 9."},"email":{"code":"validation_admin_email_exists","message":"Admin email already exists."},"password":{"code":"validation_length_out_of_range","message":"The length must be between 10 and 100."},"passwordConfirm":{"code":"validation_values_mismatch","message":"Values don't match."}}`}, - ExpectedEvents: map[string]int{ - "OnAdminBeforeCreateRequest": 1, - }, }, { Name: "authorized as admin + valid data", @@ -647,9 +641,6 @@ func TestAdminUpdate(t *testing.T) { }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{"avatar":{"code":"validation_max_less_equal_than_required","message":"Must be no greater than 9."},"email":{"code":"validation_admin_email_exists","message":"Admin email already exists."},"password":{"code":"validation_length_out_of_range","message":"The length must be between 10 and 100."},"passwordConfirm":{"code":"validation_values_mismatch","message":"Values don't match."}}`}, - ExpectedEvents: map[string]int{ - "OnAdminBeforeUpdateRequest": 1, - }, }, { Method: http.MethodPatch, diff --git a/apis/collection.go b/apis/collection.go index 7fd9b33c..af7b1dd4 100644 --- a/apis/collection.go +++ b/apis/collection.go @@ -76,9 +76,9 @@ func (api *collectionApi) create(c echo.Context) error { form := forms.NewCollectionUpsert(api.app, collection) - // read + // load request if err := c.Bind(form); err != nil { - return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) } event := &core.CollectionCreateEvent{ @@ -86,20 +86,24 @@ func (api *collectionApi) create(c echo.Context) error { Collection: collection, } - handlerErr := api.app.OnCollectionBeforeCreateRequest().Trigger(event, func(e *core.CollectionCreateEvent) error { - // submit - if err := form.Submit(); err != nil { - return rest.NewBadRequestError("Failed to create the collection.", err) - } + // create the collection + submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + return api.app.OnCollectionBeforeCreateRequest().Trigger(event, func(e *core.CollectionCreateEvent) error { + if err := next(); err != nil { + return rest.NewBadRequestError("Failed to create the collection.", err) + } - return e.HttpContext.JSON(http.StatusOK, e.Collection) + return e.HttpContext.JSON(http.StatusOK, e.Collection) + }) + } }) - if handlerErr == nil { + if submitErr == nil { api.app.OnCollectionAfterCreateRequest().Trigger(event) } - return handlerErr + return submitErr } func (api *collectionApi) update(c echo.Context) error { @@ -110,9 +114,9 @@ func (api *collectionApi) update(c echo.Context) error { form := forms.NewCollectionUpsert(api.app, collection) - // read + // load request if err := c.Bind(form); err != nil { - return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) } event := &core.CollectionUpdateEvent{ @@ -120,20 +124,24 @@ func (api *collectionApi) update(c echo.Context) error { Collection: collection, } - handlerErr := api.app.OnCollectionBeforeUpdateRequest().Trigger(event, func(e *core.CollectionUpdateEvent) error { - // submit - if err := form.Submit(); err != nil { - return rest.NewBadRequestError("Failed to update the collection.", err) - } + // update the collection + submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + return api.app.OnCollectionBeforeUpdateRequest().Trigger(event, func(e *core.CollectionUpdateEvent) error { + if err := next(); err != nil { + return rest.NewBadRequestError("Failed to update the collection.", err) + } - return e.HttpContext.JSON(http.StatusOK, e.Collection) + return e.HttpContext.JSON(http.StatusOK, e.Collection) + }) + } }) - if handlerErr == nil { + if submitErr == nil { api.app.OnCollectionAfterUpdateRequest().Trigger(event) } - return handlerErr + return submitErr } func (api *collectionApi) delete(c echo.Context) error { diff --git a/apis/collection_test.go b/apis/collection_test.go index 09f306e5..79d8a63c 100644 --- a/apis/collection_test.go +++ b/apis/collection_test.go @@ -297,9 +297,6 @@ func TestCollectionCreate(t *testing.T) { `"name":{"code":"validation_required"`, `"schema":{"code":"validation_required"`, }, - ExpectedEvents: map[string]int{ - "OnCollectionBeforeCreateRequest": 1, - }, }, { Name: "authorized as admin + invalid data (eg. existing name)", @@ -315,9 +312,6 @@ func TestCollectionCreate(t *testing.T) { `"name":{"code":"validation_collection_name_exists"`, `"schema":{"0":{"name":{"code":"validation_required"`, }, - ExpectedEvents: map[string]int{ - "OnCollectionBeforeCreateRequest": 1, - }, }, { Name: "authorized as admin + valid data", @@ -399,9 +393,6 @@ func TestCollectionUpdate(t *testing.T) { `"data":{`, `"name":{"code":"validation_collection_name_exists"`, }, - ExpectedEvents: map[string]int{ - "OnCollectionBeforeUpdateRequest": 1, - }, }, { Name: "authorized as admin + valid data", diff --git a/apis/record.go b/apis/record.go index b6f0da3b..b69c301d 100644 --- a/apis/record.go +++ b/apis/record.go @@ -193,7 +193,7 @@ func (api *recordApi) create(c echo.Context) error { testRecord := models.NewRecord(collection) testForm := forms.NewRecordUpsert(api.app, testRecord) if err := testForm.LoadData(c.Request()); err != nil { - return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) } testErr := testForm.DrySubmit(func(txDao *daos.Dao) error { @@ -210,7 +210,7 @@ func (api *recordApi) create(c echo.Context) error { // load request if err := form.LoadData(c.Request()); err != nil { - return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) } event := &core.RecordCreateEvent{ @@ -218,20 +218,24 @@ func (api *recordApi) create(c echo.Context) error { Record: record, } - handlerErr := api.app.OnRecordBeforeCreateRequest().Trigger(event, func(e *core.RecordCreateEvent) error { - // create the record - if err := form.Submit(); err != nil { - return rest.NewBadRequestError("Failed to create record.", err) - } + // create the record + submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + return api.app.OnRecordBeforeCreateRequest().Trigger(event, func(e *core.RecordCreateEvent) error { + if err := next(); err != nil { + return rest.NewBadRequestError("Failed to create record.", err) + } - return e.HttpContext.JSON(http.StatusOK, e.Record) + return e.HttpContext.JSON(http.StatusOK, e.Record) + }) + } }) - if handlerErr == nil { + if submitErr == nil { api.app.OnRecordAfterCreateRequest().Trigger(event) } - return handlerErr + return submitErr } func (api *recordApi) update(c echo.Context) error { @@ -276,7 +280,7 @@ func (api *recordApi) update(c echo.Context) error { // load request if err := form.LoadData(c.Request()); err != nil { - return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) } event := &core.RecordUpdateEvent{ @@ -284,20 +288,24 @@ func (api *recordApi) update(c echo.Context) error { Record: record, } - handlerErr := api.app.OnRecordBeforeUpdateRequest().Trigger(event, func(e *core.RecordUpdateEvent) error { - // update the record - if err := form.Submit(); err != nil { - return rest.NewBadRequestError("Failed to update record.", err) - } + // update the record + submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + return api.app.OnRecordBeforeUpdateRequest().Trigger(event, func(e *core.RecordUpdateEvent) error { + if err := next(); err != nil { + return rest.NewBadRequestError("Failed to update record.", err) + } - return e.HttpContext.JSON(http.StatusOK, e.Record) + return e.HttpContext.JSON(http.StatusOK, e.Record) + }) + } }) - if handlerErr == nil { + if submitErr == nil { api.app.OnRecordAfterUpdateRequest().Trigger(event) } - return handlerErr + return submitErr } func (api *recordApi) delete(c echo.Context) error { diff --git a/apis/settings.go b/apis/settings.go index bd1b0e8e..1f1b5b9b 100644 --- a/apis/settings.go +++ b/apis/settings.go @@ -40,6 +40,8 @@ func (api *settingsApi) list(c echo.Context) error { func (api *settingsApi) set(c echo.Context) error { form := forms.NewSettingsUpsert(api.app) + + // load request if err := c.Bind(form); err != nil { return rest.NewBadRequestError("An error occurred while reading the submitted data.", err) } @@ -50,22 +52,27 @@ func (api *settingsApi) set(c echo.Context) error { NewSettings: form.Settings, } - handlerErr := api.app.OnSettingsBeforeUpdateRequest().Trigger(event, func(e *core.SettingsUpdateEvent) error { - if err := form.Submit(); err != nil { - return rest.NewBadRequestError("An error occurred while submitting the form.", err) - } + // update the settings + submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + return api.app.OnSettingsBeforeUpdateRequest().Trigger(event, func(e *core.SettingsUpdateEvent) error { + if err := next(); err != nil { + return rest.NewBadRequestError("An error occurred while submitting the form.", err) + } - redactedSettings, err := api.app.Settings().RedactClone() - if err != nil { - return rest.NewBadRequestError("", err) - } + redactedSettings, err := api.app.Settings().RedactClone() + if err != nil { + return rest.NewBadRequestError("", err) + } - return e.HttpContext.JSON(http.StatusOK, redactedSettings) + return e.HttpContext.JSON(http.StatusOK, redactedSettings) + }) + } }) - if handlerErr == nil { + if submitErr == nil { api.app.OnSettingsAfterUpdateRequest().Trigger(event) } - return handlerErr + return submitErr } diff --git a/apis/settings_test.go b/apis/settings_test.go index afd6e65d..a9001121 100644 --- a/apis/settings_test.go +++ b/apis/settings_test.go @@ -139,9 +139,6 @@ func TestSettingsSet(t *testing.T) { `"emailAuth":{"minPasswordLength":{"code":"validation_min_greater_equal_than_required","message":"Must be no less than 5."}}`, `"meta":{"appName":{"code":"validation_required","message":"Cannot be blank."}}`, }, - ExpectedEvents: map[string]int{ - "OnSettingsBeforeUpdateRequest": 1, - }, }, { Name: "authorized as admin submitting valid data", diff --git a/apis/user.go b/apis/user.go index 2ee20b9f..43abb808 100644 --- a/apis/user.go +++ b/apis/user.go @@ -348,7 +348,7 @@ func (api *userApi) create(c echo.Context) error { // load request if err := c.Bind(form); err != nil { - return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) } event := &core.UserCreateEvent{ @@ -356,20 +356,24 @@ func (api *userApi) create(c echo.Context) error { User: user, } - handlerErr := api.app.OnUserBeforeCreateRequest().Trigger(event, func(e *core.UserCreateEvent) error { - // create the user - if err := form.Submit(); err != nil { - return rest.NewBadRequestError("Failed to create user.", err) - } + // create the user + submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + return api.app.OnUserBeforeCreateRequest().Trigger(event, func(e *core.UserCreateEvent) error { + if err := next(); err != nil { + return rest.NewBadRequestError("Failed to create user.", err) + } - return e.HttpContext.JSON(http.StatusOK, e.User) + return e.HttpContext.JSON(http.StatusOK, e.User) + }) + } }) - if handlerErr == nil { + if submitErr == nil { api.app.OnUserAfterCreateRequest().Trigger(event) } - return handlerErr + return submitErr } func (api *userApi) update(c echo.Context) error { @@ -387,7 +391,7 @@ func (api *userApi) update(c echo.Context) error { // load request if err := c.Bind(form); err != nil { - return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) } event := &core.UserUpdateEvent{ @@ -395,20 +399,24 @@ func (api *userApi) update(c echo.Context) error { User: user, } - handlerErr := api.app.OnUserBeforeUpdateRequest().Trigger(event, func(e *core.UserUpdateEvent) error { - // update the user - if err := form.Submit(); err != nil { - return rest.NewBadRequestError("Failed to update user.", err) - } + // update the user + submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + return api.app.OnUserBeforeUpdateRequest().Trigger(event, func(e *core.UserUpdateEvent) error { + if err := next(); err != nil { + return rest.NewBadRequestError("Failed to update user.", err) + } - return e.HttpContext.JSON(http.StatusOK, e.User) + return e.HttpContext.JSON(http.StatusOK, e.User) + }) + } }) - if handlerErr == nil { + if submitErr == nil { api.app.OnUserAfterUpdateRequest().Trigger(event) } - return handlerErr + return submitErr } func (api *userApi) delete(c echo.Context) error { diff --git a/apis/user_test.go b/apis/user_test.go index 76dbd62b..b42527a7 100644 --- a/apis/user_test.go +++ b/apis/user_test.go @@ -748,9 +748,6 @@ func TestUserCreate(t *testing.T) { `"email":{"code":"validation_required"`, `"password":{"code":"validation_required"`, }, - ExpectedEvents: map[string]int{ - "OnUserBeforeCreateRequest": 1, - }, }, { Name: "invalid data", @@ -764,9 +761,6 @@ func TestUserCreate(t *testing.T) { `"password":{"code":"validation_length_out_of_range"`, `"passwordConfirm":{"code":"validation_values_mismatch"`, }, - ExpectedEvents: map[string]int{ - "OnUserBeforeCreateRequest": 1, - }, }, { Name: "valid data but with disabled email/pass auth", @@ -868,9 +862,6 @@ func TestUserUpdate(t *testing.T) { `"data":{`, `"email":{"code":"validation_user_email_exists"`, }, - ExpectedEvents: map[string]int{ - "OnUserBeforeUpdateRequest": 1, - }, }, { Name: "authorized as admin - valid data", diff --git a/forms/admin_upsert.go b/forms/admin_upsert.go index 0932a4e3..143e3afa 100644 --- a/forms/admin_upsert.go +++ b/forms/admin_upsert.go @@ -74,8 +74,11 @@ func (form *AdminUpsert) checkUniqueEmail(value any) error { return validation.NewError("validation_admin_email_exists", "Admin email already exists.") } -// Submit validates the form and upserts the form's admin model. -func (form *AdminUpsert) Submit() error { +// Submit validates the form and upserts the form admin model. +// +// You can optionally provide a list of InterceptorFunc to further +// modify the form behavior before persisting it. +func (form *AdminUpsert) Submit(interceptors ...InterceptorFunc) error { if err := form.Validate(); err != nil { return err } @@ -87,5 +90,7 @@ func (form *AdminUpsert) Submit() error { form.admin.SetPassword(form.Password) } - return form.app.Dao().SaveAdmin(form.admin) + return runInterceptors(func() error { + return form.app.Dao().SaveAdmin(form.admin) + }, interceptors...) } diff --git a/forms/admin_upsert_test.go b/forms/admin_upsert_test.go index c7305283..4fa44aab 100644 --- a/forms/admin_upsert_test.go +++ b/forms/admin_upsert_test.go @@ -2,6 +2,7 @@ package forms_test import ( "encoding/json" + "errors" "testing" validation "github.com/go-ozzo/ozzo-validation/v4" @@ -252,7 +253,14 @@ func TestAdminUpsertSubmit(t *testing.T) { continue } - err := form.Submit() + interceptorCalls := 0 + + err := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptorCalls++ + return next() + } + }) hasErr := err != nil if hasErr != s.expectError { @@ -266,6 +274,14 @@ func TestAdminUpsertSubmit(t *testing.T) { continue } + expectInterceptorCall := 1 + if s.expectError { + expectInterceptorCall = 0 + } + if interceptorCalls != expectInterceptorCall { + t.Errorf("(%d) Expected interceptor to be called %d, got %d", i, expectInterceptorCall, interceptorCalls) + } + if s.expectError { continue // skip persistence check } @@ -283,3 +299,51 @@ func TestAdminUpsertSubmit(t *testing.T) { } } } + +func TestAdminUpsertSubmitInterceptors(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + admin := &models.Admin{} + form := forms.NewAdminUpsert(app, admin) + form.Email = "test_new@example.com" + form.Password = "1234567890" + form.PasswordConfirm = form.Password + + testErr := errors.New("test_error") + interceptorAdminEmail := "" + + interceptor1Called := false + interceptor1 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptor1Called = true + return next() + } + } + + interceptor2Called := false + interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptorAdminEmail = admin.Email // to check if the record was filled + interceptor2Called = true + return testErr + } + } + + err := form.Submit(interceptor1, interceptor2) + if err != testErr { + t.Fatalf("Expected error %v, got %v", testErr, err) + } + + if !interceptor1Called { + t.Fatalf("Expected interceptor1 to be called") + } + + if !interceptor2Called { + t.Fatalf("Expected interceptor2 to be called") + } + + if interceptorAdminEmail != form.Email { + t.Fatalf("Expected the form model to be filled before calling the interceptors") + } +} diff --git a/forms/base.go b/forms/base.go new file mode 100644 index 00000000..38e2ce6a --- /dev/null +++ b/forms/base.go @@ -0,0 +1,19 @@ +// Package models implements various services used for request data +// validation and applying changes to existing DB models through the app Dao. +package forms + +// InterceptorNextFunc is a interceptor handler function. +// Usually used in combination with InterceptorFunc. +type InterceptorNextFunc = func() error + +// InterceptorFunc defines a single interceptor function that will execute the provided next func handler. +type InterceptorFunc func(next InterceptorNextFunc) InterceptorNextFunc + +// runInterceptors executes the provided list of interceptors. +func runInterceptors(next InterceptorNextFunc, interceptors ...InterceptorFunc) error { + for i := len(interceptors) - 1; i >= 0; i-- { + next = interceptors[i](next) + } + + return next() +} diff --git a/forms/collection_upsert.go b/forms/collection_upsert.go index 9a05ae53..6fde8e5f 100644 --- a/forms/collection_upsert.go +++ b/forms/collection_upsert.go @@ -189,7 +189,10 @@ func (form *CollectionUpsert) checkRule(value any) error { // Submit validates the form and upserts the form's Collection model. // // On success the related record table schema will be auto updated. -func (form *CollectionUpsert) Submit() error { +// +// You can optionally provide a list of InterceptorFunc to further +// modify the form behavior before persisting it. +func (form *CollectionUpsert) Submit(interceptors ...InterceptorFunc) error { if err := form.Validate(); err != nil { return err } @@ -211,5 +214,7 @@ func (form *CollectionUpsert) Submit() error { form.collection.UpdateRule = form.UpdateRule form.collection.DeleteRule = form.DeleteRule - return form.app.Dao().SaveCollection(form.collection) + return runInterceptors(func() error { + return form.app.Dao().SaveCollection(form.collection) + }, interceptors...) } diff --git a/forms/collection_upsert_test.go b/forms/collection_upsert_test.go index ec1a9e0d..cb80ada5 100644 --- a/forms/collection_upsert_test.go +++ b/forms/collection_upsert_test.go @@ -2,6 +2,7 @@ package forms_test import ( "encoding/json" + "errors" "testing" validation "github.com/go-ozzo/ozzo-validation/v4" @@ -387,14 +388,31 @@ func TestCollectionUpsertSubmit(t *testing.T) { continue } + interceptorCalls := 0 + interceptor := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptorCalls++ + return next() + } + } + // parse errors - result := form.Submit() + result := form.Submit(interceptor) errs, ok := result.(validation.Errors) if !ok && result != nil { t.Errorf("(%d) Failed to parse errors %v", i, result) continue } + // check interceptor calls + expectInterceptorCall := 1 + if len(s.expectedErrors) > 0 { + expectInterceptorCall = 0 + } + if interceptorCalls != expectInterceptorCall { + t.Errorf("(%d) Expected interceptor to be called %d, got %d", i, expectInterceptorCall, interceptorCalls) + } + // check errors if len(errs) > len(s.expectedErrors) { t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) @@ -450,3 +468,53 @@ func TestCollectionUpsertSubmit(t *testing.T) { } } } + +func TestCollectionUpsertSubmitInterceptors(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := app.Dao().FindCollectionByNameOrId("demo") + if err != nil { + t.Fatal(err) + } + + form := forms.NewCollectionUpsert(app, collection) + form.Name = "test_new" + + testErr := errors.New("test_error") + interceptorCollectionName := "" + + interceptor1Called := false + interceptor1 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptor1Called = true + return next() + } + } + + interceptor2Called := false + interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptorCollectionName = collection.Name // to check if the record was filled + interceptor2Called = true + return testErr + } + } + + submitErr := form.Submit(interceptor1, interceptor2) + if submitErr != testErr { + t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) + } + + if !interceptor1Called { + t.Fatalf("Expected interceptor1 to be called") + } + + if !interceptor2Called { + t.Fatalf("Expected interceptor2 to be called") + } + + if interceptorCollectionName != form.Name { + t.Fatalf("Expected the form model to be filled before calling the interceptors") + } +} diff --git a/forms/record_upsert.go b/forms/record_upsert.go index eaa1d949..46f8b073 100644 --- a/forms/record_upsert.go +++ b/forms/record_upsert.go @@ -271,7 +271,10 @@ func (form *RecordUpsert) DrySubmit(callback func(txDao *daos.Dao) error) error } // Submit validates the form and upserts the form Record model. -func (form *RecordUpsert) Submit() error { +// +// You can optionally provide a list of InterceptorFunc to further +// modify the form behavior before persisting it. +func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc) error { if err := form.Validate(); err != nil { return err } @@ -281,25 +284,27 @@ func (form *RecordUpsert) Submit() error { return err } - return form.app.Dao().RunInTransaction(func(txDao *daos.Dao) error { - // persist record model - if err := txDao.SaveRecord(form.record); err != nil { - return err - } + return runInterceptors(func() error { + return form.app.Dao().RunInTransaction(func(txDao *daos.Dao) error { + // persist record model + if err := txDao.SaveRecord(form.record); err != nil { + return err + } - // upload new files (if any) - if err := form.processFilesToUpload(); err != nil { - return err - } + // upload new files (if any) + if err := form.processFilesToUpload(); err != nil { + return err + } - // delete old files (if any) - if err := form.processFilesToDelete(); err != nil { //nolint:staticcheck - // for now fail silently to avoid reupload when `form.Submit()` - // is called manually (aka. not from an api request)... - } + // delete old files (if any) + if err := form.processFilesToDelete(); err != nil { //nolint:staticcheck + // for now fail silently to avoid reupload when `form.Submit()` + // is called manually (aka. not from an api request)... + } - return nil - }) + return nil + }) + }, interceptors...) } func (form *RecordUpsert) processFilesToUpload() error { diff --git a/forms/record_upsert_test.go b/forms/record_upsert_test.go index b5bffc40..36557929 100644 --- a/forms/record_upsert_test.go +++ b/forms/record_upsert_test.go @@ -3,6 +3,7 @@ package forms_test import ( "bytes" "encoding/json" + "errors" "net/http" "net/http/httptest" "path/filepath" @@ -400,13 +401,27 @@ func TestRecordUpsertSubmitFailure(t *testing.T) { req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) form.LoadData(req) + interceptorCalls := 0 + interceptor := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptorCalls++ + return next() + } + } + // ensure that validate is triggered // --- - result := form.Submit() + result := form.Submit(interceptor) if result == nil { t.Fatal("Expected error, got nil") } + // check interceptor calls + // --- + if interceptorCalls != 0 { + t.Fatalf("Expected interceptor to be called 0 times, got %d", interceptorCalls) + } + // ensure that the record changes weren't persisted // --- recordAfter, err := app.Dao().FindFirstRecordByData(collection, "id", recordBefore.Id) @@ -451,11 +466,25 @@ func TestRecordUpsertSubmitSuccess(t *testing.T) { req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) form.LoadData(req) - result := form.Submit() + interceptorCalls := 0 + interceptor := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptorCalls++ + return next() + } + } + + result := form.Submit(interceptor) if result != nil { t.Fatalf("Expected nil, got error %v", result) } + // check interceptor calls + // --- + if interceptorCalls != 1 { + t.Fatalf("Expected interceptor to be called 1 time, got %d", interceptorCalls) + } + // ensure that the record changes were persisted // --- recordAfter, err := app.Dao().FindFirstRecordByData(collection, "id", recordBefore.Id) @@ -482,6 +511,57 @@ func TestRecordUpsertSubmitSuccess(t *testing.T) { } } +func TestRecordUpsertSubmitInterceptors(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo4") + record, err := app.Dao().FindFirstRecordByData(collection, "id", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2") + if err != nil { + t.Fatal(err) + } + + form := forms.NewRecordUpsert(app, record) + form.Data["title"] = "test_new" + + testErr := errors.New("test_error") + interceptorRecordTitle := "" + + interceptor1Called := false + interceptor1 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptor1Called = true + return next() + } + } + + interceptor2Called := false + interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptorRecordTitle = record.GetStringDataValue("title") // to check if the record was filled + interceptor2Called = true + return testErr + } + } + + submitErr := form.Submit(interceptor1, interceptor2) + if submitErr != testErr { + t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) + } + + if !interceptor1Called { + t.Fatalf("Expected interceptor1 to be called") + } + + if !interceptor2Called { + t.Fatalf("Expected interceptor2 to be called") + } + + if interceptorRecordTitle != form.Data["title"].(string) { + t.Fatalf("Expected the form model to be filled before calling the interceptors") + } +} + func hasRecordFile(app core.App, record *models.Record, filename string) bool { fs, _ := app.NewFilesystem() defer fs.Close() diff --git a/forms/settings_upsert.go b/forms/settings_upsert.go index 22dde5ca..ab2543d5 100644 --- a/forms/settings_upsert.go +++ b/forms/settings_upsert.go @@ -33,27 +33,32 @@ func (form *SettingsUpsert) Validate() error { // Submit validates the form and upserts the loaded settings. // // On success the app settings will be refreshed with the form ones. -func (form *SettingsUpsert) Submit() error { +// +// You can optionally provide a list of InterceptorFunc to further +// modify the form behavior before persisting it. +func (form *SettingsUpsert) Submit(interceptors ...InterceptorFunc) error { if err := form.Validate(); err != nil { return err } encryptionKey := os.Getenv(form.app.EncryptionEnv()) - saveErr := form.app.Dao().SaveParam( - models.ParamAppSettings, - form.Settings, - encryptionKey, - ) - if saveErr != nil { - return saveErr - } + return runInterceptors(func() error { + saveErr := form.app.Dao().SaveParam( + models.ParamAppSettings, + form.Settings, + encryptionKey, + ) + if saveErr != nil { + return saveErr + } - // explicitly trigger old logs deletion - form.app.LogsDao().DeleteOldRequests( - time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays), - ) + // explicitly trigger old logs deletion + form.app.LogsDao().DeleteOldRequests( + time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays), + ) - // merge the application settings with the form ones - return form.app.Settings().Merge(form.Settings) + // merge the application settings with the form ones + return form.app.Settings().Merge(form.Settings) + }, interceptors...) } diff --git a/forms/settings_upsert_test.go b/forms/settings_upsert_test.go index 01c838cd..d0926052 100644 --- a/forms/settings_upsert_test.go +++ b/forms/settings_upsert_test.go @@ -2,6 +2,7 @@ package forms_test import ( "encoding/json" + "errors" "os" "testing" @@ -98,14 +99,31 @@ func TestSettingsUpsertSubmit(t *testing.T) { continue } + interceptorCalls := 0 + interceptor := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptorCalls++ + return next() + } + } + // parse errors - result := form.Submit() + result := form.Submit(interceptor) errs, ok := result.(validation.Errors) if !ok && result != nil { t.Errorf("(%d) Failed to parse errors %v", i, result) continue } + // check interceptor calls + expectInterceptorCall := 1 + if len(s.expectedErrors) > 0 { + expectInterceptorCall = 0 + } + if interceptorCalls != expectInterceptorCall { + t.Errorf("(%d) Expected interceptor to be called %d, got %d", i, expectInterceptorCall, interceptorCalls) + } + // check errors if len(errs) > len(s.expectedErrors) { t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) @@ -128,3 +146,42 @@ func TestSettingsUpsertSubmit(t *testing.T) { } } } + +func TestSettingsUpsertSubmitInterceptors(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + form := forms.NewSettingsUpsert(app) + form.Meta.AppName = "test_new" + + testErr := errors.New("test_error") + + interceptor1Called := false + interceptor1 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptor1Called = true + return next() + } + } + + interceptor2Called := false + interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptor2Called = true + return testErr + } + } + + submitErr := form.Submit(interceptor1, interceptor2) + if submitErr != testErr { + t.Fatalf("Expected submitError %v, got %v", testErr, submitErr) + } + + if !interceptor1Called { + t.Fatalf("Expected interceptor1 to be called") + } + + if !interceptor2Called { + t.Fatalf("Expected interceptor2 to be called") + } +} diff --git a/forms/user_upsert.go b/forms/user_upsert.go index c78a3e10..7dbd12ec 100644 --- a/forms/user_upsert.go +++ b/forms/user_upsert.go @@ -98,7 +98,10 @@ func (form *UserUpsert) checkEmailDomain(value any) error { } // Submit validates the form and upserts the form user model. -func (form *UserUpsert) Submit() error { +// +// You can optionally provide a list of InterceptorFunc to further +// modify the form behavior before persisting it. +func (form *UserUpsert) Submit(interceptors ...InterceptorFunc) error { if err := form.Validate(); err != nil { return err } @@ -114,5 +117,7 @@ func (form *UserUpsert) Submit() error { form.user.Email = form.Email - return form.app.Dao().SaveUser(form.user) + return runInterceptors(func() error { + return form.app.Dao().SaveUser(form.user) + }, interceptors...) } diff --git a/forms/user_upsert_test.go b/forms/user_upsert_test.go index 4612c180..4451415c 100644 --- a/forms/user_upsert_test.go +++ b/forms/user_upsert_test.go @@ -2,6 +2,7 @@ package forms_test import ( "encoding/json" + "errors" "testing" validation "github.com/go-ozzo/ozzo-validation/v4" @@ -212,13 +213,28 @@ func TestUserUpsertSubmit(t *testing.T) { continue } - err := form.Submit() + interceptorCalls := 0 + + err := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptorCalls++ + return next() + } + }) hasErr := err != nil if hasErr != s.expectError { t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) } + expectInterceptorCall := 1 + if s.expectError { + expectInterceptorCall = 0 + } + if interceptorCalls != expectInterceptorCall { + t.Errorf("(%d) Expected interceptor to be called %d, got %d", i, expectInterceptorCall, interceptorCalls) + } + if s.expectError { continue } @@ -240,3 +256,51 @@ func TestUserUpsertSubmit(t *testing.T) { } } } + +func TestUserUpsertSubmitInterceptors(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + user := &models.User{} + form := forms.NewUserUpsert(app, user) + form.Email = "test_new@example.com" + form.Password = "1234567890" + form.PasswordConfirm = form.Password + + testErr := errors.New("test_error") + interceptorUserEmail := "" + + interceptor1Called := false + interceptor1 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptor1Called = true + return next() + } + } + + interceptor2Called := false + interceptor2 := func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc { + return func() error { + interceptorUserEmail = user.Email // to check if the record was filled + interceptor2Called = true + return testErr + } + } + + err := form.Submit(interceptor1, interceptor2) + if err != testErr { + t.Fatalf("Expected error %v, got %v", testErr, err) + } + + if !interceptor1Called { + t.Fatalf("Expected interceptor1 to be called") + } + + if !interceptor2Called { + t.Fatalf("Expected interceptor2 to be called") + } + + if interceptorUserEmail != form.Email { + t.Fatalf("Expected the form model to be filled before calling the interceptors") + } +}