1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-02-13 16:31:59 +02:00

before updateding test data

This commit is contained in:
Gani Georgiev 2022-08-07 20:58:21 +03:00
parent a426484916
commit 6e9d000426
16 changed files with 323 additions and 75 deletions

View File

@ -169,19 +169,37 @@ func (api *collectionApi) delete(c echo.Context) error {
return handlerErr return handlerErr
} }
// @todo add event
func (api *collectionApi) bulkImport(c echo.Context) error { func (api *collectionApi) bulkImport(c echo.Context) error {
form := forms.NewCollectionsImport(api.app) form := forms.NewCollectionsImport(api.app)
// load request // load request data
if err := c.Bind(form); err != nil { if err := c.Bind(form); err != nil {
return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err) return rest.NewBadRequestError("Failed to load the submitted data due to invalid formatting.", err)
} }
submitErr := form.Submit() event := &core.CollectionsImportEvent{
if submitErr != nil { HttpContext: c,
return rest.NewBadRequestError("Failed to import the submitted collections.", submitErr) Collections: form.Collections,
} }
return c.NoContent(http.StatusNoContent) // import collections
submitErr := form.Submit(func(next forms.InterceptorNextFunc) forms.InterceptorNextFunc {
return func() error {
return api.app.OnCollectionsBeforeImportRequest().Trigger(event, func(e *core.CollectionsImportEvent) error {
form.Collections = e.Collections // ensures that the form always has the latest changes
if err := next(); err != nil {
return rest.NewBadRequestError("Failed to import the submitted collections.", err)
}
return e.HttpContext.NoContent(http.StatusNoContent)
})
}
})
if submitErr == nil {
api.app.OnCollectionsAfterImportRequest().Trigger(event)
}
return submitErr
} }

View File

@ -440,3 +440,57 @@ func TestCollectionUpdate(t *testing.T) {
scenario.Test(t) scenario.Test(t)
} }
} }
func TestCollectionImport(t *testing.T) {
scenarios := []tests.ApiScenario{
{
Name: "unauthorized",
Method: http.MethodPut,
Url: "/api/collections/import",
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as user",
Method: http.MethodPut,
Url: "/api/collections/import",
RequestHeaders: map[string]string{
"Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic",
},
ExpectedStatus: 401,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "authorized as admin + empty collections",
Method: http.MethodPut,
Url: "/api/collections/import",
Body: strings.NewReader(`{"collections":[]}`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"collections":{"code":"validation_required"`,
},
},
{
Name: "authorized as admin + deleting collections",
Method: http.MethodPut,
Url: "/api/collections/import",
Body: strings.NewReader(`{"collections":[]}`),
RequestHeaders: map[string]string{
"Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"collections":{"code":"validation_required"`,
},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}

View File

@ -119,7 +119,7 @@ type App interface {
// sending a password reset email to an admin. // sending a password reset email to an admin.
// //
// Could be used to send your own custom email template if // Could be used to send your own custom email template if
// hook.StopPropagation is returned in one of its listeners. // [hook.StopPropagation] is returned in one of its listeners.
OnMailerBeforeAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent] OnMailerBeforeAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent]
// OnMailerAfterAdminResetPasswordSend hook is triggered after // OnMailerAfterAdminResetPasswordSend hook is triggered after
@ -130,7 +130,7 @@ type App interface {
// sending a password reset email to a user. // sending a password reset email to a user.
// //
// Could be used to send your own custom email template if // Could be used to send your own custom email template if
// hook.StopPropagation is returned in one of its listeners. // [hook.StopPropagation] is returned in one of its listeners.
OnMailerBeforeUserResetPasswordSend() *hook.Hook[*MailerUserEvent] OnMailerBeforeUserResetPasswordSend() *hook.Hook[*MailerUserEvent]
// OnMailerAfterUserResetPasswordSend hook is triggered after // OnMailerAfterUserResetPasswordSend hook is triggered after
@ -141,7 +141,7 @@ type App interface {
// sending a verification email to a user. // sending a verification email to a user.
// //
// Could be used to send your own custom email template if // Could be used to send your own custom email template if
// hook.StopPropagation is returned in one of its listeners. // [hook.StopPropagation] is returned in one of its listeners.
OnMailerBeforeUserVerificationSend() *hook.Hook[*MailerUserEvent] OnMailerBeforeUserVerificationSend() *hook.Hook[*MailerUserEvent]
// OnMailerAfterUserVerificationSend hook is triggered after a user // OnMailerAfterUserVerificationSend hook is triggered after a user
@ -152,7 +152,7 @@ type App interface {
// sending a confirmation new address email to a a user. // sending a confirmation new address email to a a user.
// //
// Could be used to send your own custom email template if // Could be used to send your own custom email template if
// hook.StopPropagation is returned in one of its listeners. // [hook.StopPropagation] is returned in one of its listeners.
OnMailerBeforeUserChangeEmailSend() *hook.Hook[*MailerUserEvent] OnMailerBeforeUserChangeEmailSend() *hook.Hook[*MailerUserEvent]
// OnMailerAfterUserChangeEmailSend hook is triggered after a user // OnMailerAfterUserChangeEmailSend hook is triggered after a user
@ -192,7 +192,7 @@ type App interface {
// //
// Could be used to additionally validate the request data or // Could be used to additionally validate the request data or
// implement completely different persistence behavior // implement completely different persistence behavior
// (returning hook.StopPropagation). // (returning [hook.StopPropagation]).
OnSettingsBeforeUpdateRequest() *hook.Hook[*SettingsUpdateEvent] OnSettingsBeforeUpdateRequest() *hook.Hook[*SettingsUpdateEvent]
// OnSettingsAfterUpdateRequest hook is triggered after each // OnSettingsAfterUpdateRequest hook is triggered after each
@ -227,7 +227,7 @@ type App interface {
// Admin create request (after request data load and before model persistence). // Admin create request (after request data load and before model persistence).
// //
// Could be used to additionally validate the request data or implement // Could be used to additionally validate the request data or implement
// completely different persistence behavior (returning hook.StopPropagation). // completely different persistence behavior (returning [hook.StopPropagation]).
OnAdminBeforeCreateRequest() *hook.Hook[*AdminCreateEvent] OnAdminBeforeCreateRequest() *hook.Hook[*AdminCreateEvent]
// OnAdminAfterCreateRequest hook is triggered after each // OnAdminAfterCreateRequest hook is triggered after each
@ -238,7 +238,7 @@ type App interface {
// Admin update request (after request data load and before model persistence). // Admin update request (after request data load and before model persistence).
// //
// Could be used to additionally validate the request data or implement // Could be used to additionally validate the request data or implement
// completely different persistence behavior (returning hook.StopPropagation). // completely different persistence behavior (returning [hook.StopPropagation]).
OnAdminBeforeUpdateRequest() *hook.Hook[*AdminUpdateEvent] OnAdminBeforeUpdateRequest() *hook.Hook[*AdminUpdateEvent]
// OnAdminAfterUpdateRequest hook is triggered after each // OnAdminAfterUpdateRequest hook is triggered after each
@ -249,7 +249,7 @@ type App interface {
// Admin delete request (after model load and before actual deletion). // Admin delete request (after model load and before actual deletion).
// //
// Could be used to additionally validate the request data or implement // Could be used to additionally validate the request data or implement
// completely different delete behavior (returning hook.StopPropagation). // completely different delete behavior (returning [hook.StopPropagation]).
OnAdminBeforeDeleteRequest() *hook.Hook[*AdminDeleteEvent] OnAdminBeforeDeleteRequest() *hook.Hook[*AdminDeleteEvent]
// OnAdminAfterDeleteRequest hook is triggered after each // OnAdminAfterDeleteRequest hook is triggered after each
@ -281,7 +281,7 @@ type App interface {
// create request (after request data load and before model persistence). // create request (after request data load and before model persistence).
// //
// Could be used to additionally validate the request data or implement // Could be used to additionally validate the request data or implement
// completely different persistence behavior (returning hook.StopPropagation). // completely different persistence behavior (returning [hook.StopPropagation]).
OnUserBeforeCreateRequest() *hook.Hook[*UserCreateEvent] OnUserBeforeCreateRequest() *hook.Hook[*UserCreateEvent]
// OnUserAfterCreateRequest hook is triggered after each // OnUserAfterCreateRequest hook is triggered after each
@ -292,7 +292,7 @@ type App interface {
// update request (after request data load and before model persistence). // update request (after request data load and before model persistence).
// //
// Could be used to additionally validate the request data or implement // Could be used to additionally validate the request data or implement
// completely different persistence behavior (returning hook.StopPropagation). // completely different persistence behavior (returning [hook.StopPropagation]).
OnUserBeforeUpdateRequest() *hook.Hook[*UserUpdateEvent] OnUserBeforeUpdateRequest() *hook.Hook[*UserUpdateEvent]
// OnUserAfterUpdateRequest hook is triggered after each // OnUserAfterUpdateRequest hook is triggered after each
@ -303,7 +303,7 @@ type App interface {
// delete request (after model load and before actual deletion). // delete request (after model load and before actual deletion).
// //
// Could be used to additionally validate the request data or implement // Could be used to additionally validate the request data or implement
// completely different delete behavior (returning hook.StopPropagation). // completely different delete behavior (returning [hook.StopPropagation]).
OnUserBeforeDeleteRequest() *hook.Hook[*UserDeleteEvent] OnUserBeforeDeleteRequest() *hook.Hook[*UserDeleteEvent]
// OnUserAfterDeleteRequest hook is triggered after each // OnUserAfterDeleteRequest hook is triggered after each
@ -346,7 +346,7 @@ type App interface {
// create request (after request data load and before model persistence). // create request (after request data load and before model persistence).
// //
// Could be used to additionally validate the request data or implement // Could be used to additionally validate the request data or implement
// completely different persistence behavior (returning hook.StopPropagation). // completely different persistence behavior (returning [hook.StopPropagation]).
OnRecordBeforeCreateRequest() *hook.Hook[*RecordCreateEvent] OnRecordBeforeCreateRequest() *hook.Hook[*RecordCreateEvent]
// OnRecordAfterCreateRequest hook is triggered after each // OnRecordAfterCreateRequest hook is triggered after each
@ -357,7 +357,7 @@ type App interface {
// update request (after request data load and before model persistence). // update request (after request data load and before model persistence).
// //
// Could be used to additionally validate the request data or implement // Could be used to additionally validate the request data or implement
// completely different persistence behavior (returning hook.StopPropagation). // completely different persistence behavior (returning [hook.StopPropagation]).
OnRecordBeforeUpdateRequest() *hook.Hook[*RecordUpdateEvent] OnRecordBeforeUpdateRequest() *hook.Hook[*RecordUpdateEvent]
// OnRecordAfterUpdateRequest hook is triggered after each // OnRecordAfterUpdateRequest hook is triggered after each
@ -368,7 +368,7 @@ type App interface {
// delete request (after model load and before actual deletion). // delete request (after model load and before actual deletion).
// //
// Could be used to additionally validate the request data or implement // Could be used to additionally validate the request data or implement
// completely different delete behavior (returning hook.StopPropagation). // completely different delete behavior (returning [hook.StopPropagation]).
OnRecordBeforeDeleteRequest() *hook.Hook[*RecordDeleteEvent] OnRecordBeforeDeleteRequest() *hook.Hook[*RecordDeleteEvent]
// OnRecordAfterDeleteRequest hook is triggered after each // OnRecordAfterDeleteRequest hook is triggered after each
@ -393,7 +393,7 @@ type App interface {
// create request (after request data load and before model persistence). // create request (after request data load and before model persistence).
// //
// Could be used to additionally validate the request data or implement // Could be used to additionally validate the request data or implement
// completely different persistence behavior (returning hook.StopPropagation). // completely different persistence behavior (returning [hook.StopPropagation]).
OnCollectionBeforeCreateRequest() *hook.Hook[*CollectionCreateEvent] OnCollectionBeforeCreateRequest() *hook.Hook[*CollectionCreateEvent]
// OnCollectionAfterCreateRequest hook is triggered after each // OnCollectionAfterCreateRequest hook is triggered after each
@ -404,7 +404,7 @@ type App interface {
// update request (after request data load and before model persistence). // update request (after request data load and before model persistence).
// //
// Could be used to additionally validate the request data or implement // Could be used to additionally validate the request data or implement
// completely different persistence behavior (returning hook.StopPropagation). // completely different persistence behavior (returning [hook.StopPropagation]).
OnCollectionBeforeUpdateRequest() *hook.Hook[*CollectionUpdateEvent] OnCollectionBeforeUpdateRequest() *hook.Hook[*CollectionUpdateEvent]
// OnCollectionAfterUpdateRequest hook is triggered after each // OnCollectionAfterUpdateRequest hook is triggered after each
@ -415,10 +415,21 @@ type App interface {
// Collection delete request (after model load and before actual deletion). // Collection delete request (after model load and before actual deletion).
// //
// Could be used to additionally validate the request data or implement // Could be used to additionally validate the request data or implement
// completely different delete behavior (returning hook.StopPropagation). // completely different delete behavior (returning [hook.StopPropagation]).
OnCollectionBeforeDeleteRequest() *hook.Hook[*CollectionDeleteEvent] OnCollectionBeforeDeleteRequest() *hook.Hook[*CollectionDeleteEvent]
// OnCollectionAfterDeleteRequest hook is triggered after each // OnCollectionAfterDeleteRequest hook is triggered after each
// successful API Collection delete request. // successful API Collection delete request.
OnCollectionAfterDeleteRequest() *hook.Hook[*CollectionDeleteEvent] OnCollectionAfterDeleteRequest() *hook.Hook[*CollectionDeleteEvent]
// OnCollectionsBeforeImportRequest hook is triggered before each API
// collections import request (after request data load and before the actual import).
//
// Could be used to additionally validate the imported collections or
// to implement completely different import behavior (returning [hook.StopPropagation]).
OnCollectionsBeforeImportRequest() *hook.Hook[*CollectionsImportEvent]
// OnCollectionsAfterImportRequest hook is triggered after each
// successful API collections import request.
OnCollectionsAfterImportRequest() *hook.Hook[*CollectionsImportEvent]
} }

View File

@ -109,14 +109,16 @@ type BaseApp struct {
onRecordAfterDeleteRequest *hook.Hook[*RecordDeleteEvent] onRecordAfterDeleteRequest *hook.Hook[*RecordDeleteEvent]
// collection api event hooks // collection api event hooks
onCollectionsListRequest *hook.Hook[*CollectionsListEvent] onCollectionsListRequest *hook.Hook[*CollectionsListEvent]
onCollectionViewRequest *hook.Hook[*CollectionViewEvent] onCollectionViewRequest *hook.Hook[*CollectionViewEvent]
onCollectionBeforeCreateRequest *hook.Hook[*CollectionCreateEvent] onCollectionBeforeCreateRequest *hook.Hook[*CollectionCreateEvent]
onCollectionAfterCreateRequest *hook.Hook[*CollectionCreateEvent] onCollectionAfterCreateRequest *hook.Hook[*CollectionCreateEvent]
onCollectionBeforeUpdateRequest *hook.Hook[*CollectionUpdateEvent] onCollectionBeforeUpdateRequest *hook.Hook[*CollectionUpdateEvent]
onCollectionAfterUpdateRequest *hook.Hook[*CollectionUpdateEvent] onCollectionAfterUpdateRequest *hook.Hook[*CollectionUpdateEvent]
onCollectionBeforeDeleteRequest *hook.Hook[*CollectionDeleteEvent] onCollectionBeforeDeleteRequest *hook.Hook[*CollectionDeleteEvent]
onCollectionAfterDeleteRequest *hook.Hook[*CollectionDeleteEvent] onCollectionAfterDeleteRequest *hook.Hook[*CollectionDeleteEvent]
onCollectionsBeforeImportRequest *hook.Hook[*CollectionsImportEvent]
onCollectionsAfterImportRequest *hook.Hook[*CollectionsImportEvent]
} }
// NewBaseApp creates and returns a new BaseApp instance // NewBaseApp creates and returns a new BaseApp instance
@ -201,14 +203,16 @@ func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp {
onRecordAfterDeleteRequest: &hook.Hook[*RecordDeleteEvent]{}, onRecordAfterDeleteRequest: &hook.Hook[*RecordDeleteEvent]{},
// collection API event hooks // collection API event hooks
onCollectionsListRequest: &hook.Hook[*CollectionsListEvent]{}, onCollectionsListRequest: &hook.Hook[*CollectionsListEvent]{},
onCollectionViewRequest: &hook.Hook[*CollectionViewEvent]{}, onCollectionViewRequest: &hook.Hook[*CollectionViewEvent]{},
onCollectionBeforeCreateRequest: &hook.Hook[*CollectionCreateEvent]{}, onCollectionBeforeCreateRequest: &hook.Hook[*CollectionCreateEvent]{},
onCollectionAfterCreateRequest: &hook.Hook[*CollectionCreateEvent]{}, onCollectionAfterCreateRequest: &hook.Hook[*CollectionCreateEvent]{},
onCollectionBeforeUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{}, onCollectionBeforeUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{},
onCollectionAfterUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{}, onCollectionAfterUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{},
onCollectionBeforeDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{}, onCollectionBeforeDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{},
onCollectionAfterDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{}, onCollectionAfterDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{},
onCollectionsBeforeImportRequest: &hook.Hook[*CollectionsImportEvent]{},
onCollectionsAfterImportRequest: &hook.Hook[*CollectionsImportEvent]{},
} }
app.registerDefaultHooks() app.registerDefaultHooks()
@ -687,6 +691,14 @@ func (app *BaseApp) OnCollectionAfterDeleteRequest() *hook.Hook[*CollectionDelet
return app.onCollectionAfterDeleteRequest return app.onCollectionAfterDeleteRequest
} }
func (app *BaseApp) OnCollectionsBeforeImportRequest() *hook.Hook[*CollectionsImportEvent] {
return app.onCollectionsBeforeImportRequest
}
func (app *BaseApp) OnCollectionsAfterImportRequest() *hook.Hook[*CollectionsImportEvent] {
return app.onCollectionsAfterImportRequest
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Helpers // Helpers
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View File

@ -216,6 +216,11 @@ type CollectionDeleteEvent struct {
Collection *models.Collection Collection *models.Collection
} }
type CollectionsImportEvent struct {
HttpContext echo.Context
Collections []*models.Collection
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// File API events data // File API events data
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View File

@ -128,11 +128,11 @@ func (dao *Dao) Delete(m models.Model) error {
// Save upserts (update or create if primary key is not set) the provided model. // Save upserts (update or create if primary key is not set) the provided model.
func (dao *Dao) Save(m models.Model) error { func (dao *Dao) Save(m models.Model) error {
if !m.IsNew() { if m.IsNew() {
return dao.update(m) return dao.create(m)
} }
return dao.create(m) return dao.update(m)
} }
func (dao *Dao) update(m models.Model) error { func (dao *Dao) update(m models.Model) error {

View File

@ -158,6 +158,33 @@ func TestDaoSaveCreate(t *testing.T) {
} }
} }
func TestDaoSaveWithInsertId(t *testing.T) {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
model := &models.Admin{}
model.Id = "test"
model.Email = "test_new@example.com"
model.MarkAsNew()
if err := testApp.Dao().Save(model); err != nil {
t.Fatal(err)
}
// refresh
model, _ = testApp.Dao().FindAdminById("test")
if model == nil {
t.Fatal("Failed to find admin with id 'test'")
}
expectedHooks := []string{"OnModelBeforeCreate", "OnModelAfterCreate"}
for _, h := range expectedHooks {
if v, ok := testApp.EventCalls[h]; !ok || v != 1 {
t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v)
}
}
}
func TestDaoSaveUpdate(t *testing.T) { func TestDaoSaveUpdate(t *testing.T) {
testApp, _ := tests.NewTestApp() testApp, _ := tests.NewTestApp()
defer testApp.Cleanup() defer testApp.Cleanup()
@ -184,6 +211,61 @@ func TestDaoSaveUpdate(t *testing.T) {
} }
} }
type dummyColumnValueMapper struct {
models.Admin
}
func (a *dummyColumnValueMapper) ColumnValueMap() map[string]any {
return map[string]any{
"email": a.Email,
"passwordHash": a.PasswordHash,
"tokenKey": "custom_token_key",
}
}
func TestDaoSaveWithColumnValueMapper(t *testing.T) {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
model := &dummyColumnValueMapper{}
model.Id = "test_mapped_id" // explicitly set an id
model.Email = "test_mapped_create@example.com"
model.TokenKey = "test_unmapped_token_key" // not used in the map
model.SetPassword("123456")
model.MarkAsNew()
if err := testApp.Dao().Save(model); err != nil {
t.Fatal(err)
}
createdModel, _ := testApp.Dao().FindAdminById("test_mapped_id")
if createdModel == nil {
t.Fatal("[create] Failed to find model with id 'test_mapped_id'")
}
if createdModel.Email != model.Email {
t.Fatalf("Expected model with email %q, got %q", model.Email, createdModel.Email)
}
if createdModel.TokenKey != "custom_token_key" {
t.Fatalf("Expected model with tokenKey %q, got %q", "custom_token_key", createdModel.TokenKey)
}
model.Email = "test_mapped_update@example.com"
model.Avatar = 9 // not mapped and expect to be ignored
if err := testApp.Dao().Save(model); err != nil {
t.Fatal(err)
}
updatedModel, _ := testApp.Dao().FindAdminById("test_mapped_id")
if updatedModel == nil {
t.Fatal("[update] Failed to find model with id 'test_mapped_id'")
}
if updatedModel.Email != model.Email {
t.Fatalf("Expected model with email %q, got %q", model.Email, createdModel.Email)
}
if updatedModel.Avatar != 0 {
t.Fatalf("Expected model avatar 0, got %v", updatedModel.Avatar)
}
}
func TestDaoDelete(t *testing.T) { func TestDaoDelete(t *testing.T) {
testApp, _ := tests.NewTestApp() testApp, _ := tests.NewTestApp()
defer testApp.Cleanup() defer testApp.Cleanup()

View File

@ -70,11 +70,18 @@ func (form *CollectionsImport) Validate() error {
// //
// All operations are wrapped in a single transaction that are // All operations are wrapped in a single transaction that are
// rollbacked on the first encountered error. // rollbacked on the first encountered error.
func (form *CollectionsImport) Submit() error { //
// You can optionally provide a list of InterceptorFunc to further
// modify the form behavior before persisting it.
func (form *CollectionsImport) Submit(interceptors ...InterceptorFunc) error {
if err := form.Validate(); err != nil { if err := form.Validate(); err != nil {
return err return err
} }
return runInterceptors(form.submit, interceptors...)
}
func (form *CollectionsImport) submit() error {
return form.config.TxDao.RunInTransaction(func(txDao *daos.Dao) error { return form.config.TxDao.RunInTransaction(func(txDao *daos.Dao) error {
oldCollections := []*models.Collection{} oldCollections := []*models.Collection{}
if err := txDao.CollectionQuery().All(&oldCollections); err != nil { if err := txDao.CollectionQuery().All(&oldCollections); err != nil {

View File

@ -34,25 +34,55 @@ func TestBaseModelHasId(t *testing.T) {
} }
} }
func TestBaseModelGetId(t *testing.T) { func TestBaseModelId(t *testing.T) {
m0 := models.BaseModel{} m := models.BaseModel{}
if m0.GetId() != "" {
t.Fatalf("Expected zero id value, got %v", m0.GetId()) if m.GetId() != "" {
t.Fatalf("Expected empty id value, got %v", m.GetId())
} }
id := "abc" m.SetId("test")
m1 := models.BaseModel{Id: id}
if m1.GetId() != id { if m.GetId() != "test" {
t.Fatalf("Expected id %v, got %v", id, m1.GetId()) t.Fatalf("Expected %q id, got %v", "test", m.GetId())
}
m.RefreshId()
if len(m.GetId()) != 15 {
t.Fatalf("Expected 15 chars id, got %v", m.GetId())
} }
} }
func TestBaseModelRefreshId(t *testing.T) { func TestBaseModelIsNew(t *testing.T) {
m := models.BaseModel{} m0 := models.BaseModel{}
m.RefreshId() m1 := models.BaseModel{Id: ""}
m2 := models.BaseModel{Id: "test"}
m3 := models.BaseModel{}
m3.MarkAsNew()
m4 := models.BaseModel{Id: "test"}
m4.MarkAsNew()
m5 := models.BaseModel{Id: "test"}
m5.MarkAsNew()
m5.UnmarkAsNew()
if m.GetId() == "" { scenarios := []struct {
t.Fatalf("Expected nonempty id value, got %v", m.GetId()) model models.BaseModel
expected bool
}{
{m0, true},
{m1, true},
{m2, false},
{m3, true},
{m4, true},
{m5, false},
}
for i, s := range scenarios {
result := s.model.IsNew()
if result != s.expected {
t.Errorf("(%d) Expected IsNew %v, got %v", i, s.expected, result)
}
} }
} }

View File

@ -313,6 +313,16 @@ func NewTestApp() (*TestApp, error) {
return nil return nil
}) })
t.OnCollectionsBeforeImportRequest().Add(func(e *core.CollectionsImportEvent) error {
t.EventCalls["OnCollectionsBeforeImportRequest"]++
return nil
})
t.OnCollectionsAfterImportRequest().Add(func(e *core.CollectionsImportEvent) error {
t.EventCalls["OnCollectionsAfterImportRequest"]++
return nil
})
t.OnAdminsListRequest().Add(func(e *core.AdminsListEvent) error { t.OnAdminsListRequest().Add(func(e *core.AdminsListEvent) error {
t.EventCalls["OnAdminsListRequest"]++ t.EventCalls["OnAdminsListRequest"]++
return nil return nil

View File

@ -1 +1 @@
PB_BACKEND_URL = http://localhost:8090 PB_BACKEND_URL = http://127.0.0.1:8090

2
ui/dist/index.html vendored
View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://localhost:* data:; connect-src 'self' http://localhost:*; script-src 'self' 'sha256-GRUzBA7PzKYug7pqxv5rJaec5bwDCw1Vo6/IXwvD3Tc='" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://127.0.0.1:* data:; connect-src 'self' http://127.0.0.1:*; script-src 'self' 'sha256-GRUzBA7PzKYug7pqxv5rJaec5bwDCw1Vo6/IXwvD3Tc='"
/> />
<title>PocketBase</title> <title>PocketBase</title>

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://localhost:* data:; connect-src 'self' http://localhost:*; script-src 'self' 'sha256-GRUzBA7PzKYug7pqxv5rJaec5bwDCw1Vo6/IXwvD3Tc='" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://127.0.0.1:* data:; connect-src 'self' http://127.0.0.1:*; script-src 'self' 'sha256-GRUzBA7PzKYug7pqxv5rJaec5bwDCw1Vo6/IXwvD3Tc='"
/> />
<title>PocketBase</title> <title>PocketBase</title>

28
ui/package-lock.json generated
View File

@ -938,9 +938,9 @@
} }
}, },
"node_modules/regexparam": { "node_modules/regexparam": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.0.tgz", "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.1.tgz",
"integrity": "sha512-gJKwd2MVPWHAIFLsaYDZfyKzHNS4o7E/v8YmNf44vmeV2e4YfVoDToTOKTvE7ab68cRJ++kLuEXJBaEeJVt5ow==", "integrity": "sha512-zRgSaYemnNYxUv+/5SeoHI0eJIgTL/A2pUtXUPLHQxUldagouJ9p+K6IbIZ/JiQuCEv2E2B1O11SjVQy3aMCkw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -1062,12 +1062,12 @@
} }
}, },
"node_modules/svelte-spa-router": { "node_modules/svelte-spa-router": {
"version": "3.2.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-3.2.0.tgz", "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-3.3.0.tgz",
"integrity": "sha512-igemo5Vs82TGBBw+DjWt6qKameXYzNs6aDXcTxou5XbEvOjiRcAM6MLkdVRCatn6u8r42dE99bt/br7T4qe/AQ==", "integrity": "sha512-cwRNe7cxD43sCvSfEeaKiNZg3FCizGxeMcf7CPiWRP3jKXjEma3vxyyuDtPOam6nWbVxl9TNM3hlE/i87ZlqcQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"regexparam": "2.0.0" "regexparam": "2.0.1"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ItalyPaleAle" "url": "https://github.com/sponsors/ItalyPaleAle"
@ -1714,9 +1714,9 @@
} }
}, },
"regexparam": { "regexparam": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.0.tgz", "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.1.tgz",
"integrity": "sha512-gJKwd2MVPWHAIFLsaYDZfyKzHNS4o7E/v8YmNf44vmeV2e4YfVoDToTOKTvE7ab68cRJ++kLuEXJBaEeJVt5ow==", "integrity": "sha512-zRgSaYemnNYxUv+/5SeoHI0eJIgTL/A2pUtXUPLHQxUldagouJ9p+K6IbIZ/JiQuCEv2E2B1O11SjVQy3aMCkw==",
"dev": true "dev": true
}, },
"resolve": { "resolve": {
@ -1797,12 +1797,12 @@
"requires": {} "requires": {}
}, },
"svelte-spa-router": { "svelte-spa-router": {
"version": "3.2.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-3.2.0.tgz", "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-3.3.0.tgz",
"integrity": "sha512-igemo5Vs82TGBBw+DjWt6qKameXYzNs6aDXcTxou5XbEvOjiRcAM6MLkdVRCatn6u8r42dE99bt/br7T4qe/AQ==", "integrity": "sha512-cwRNe7cxD43sCvSfEeaKiNZg3FCizGxeMcf7CPiWRP3jKXjEma3vxyyuDtPOam6nWbVxl9TNM3hlE/i87ZlqcQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"regexparam": "2.0.0" "regexparam": "2.0.1"
} }
}, },
"to-regex-range": { "to-regex-range": {

View File

@ -1,5 +1,8 @@
// https://github.com/google/diff-match-patch /**
// https://github.com/google/diff-match-patch/blob/master/LICENSE * diff-match-patch
* https://github.com/google/diff-match-patch
* https://github.com/google/diff-match-patch/blob/master/LICENSE
*/
var diff_match_patch=function(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=.5;this.Patch_Margin=4;this.Match_MaxBits=32},DIFF_DELETE=-1,DIFF_INSERT=1,DIFF_EQUAL=0;diff_match_patch.Diff=function(a,b){this[0]=a;this[1]=b};diff_match_patch.Diff.prototype.length=2;diff_match_patch.Diff.prototype.toString=function(){return this[0]+","+this[1]}; var diff_match_patch=function(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=.5;this.Patch_Margin=4;this.Match_MaxBits=32},DIFF_DELETE=-1,DIFF_INSERT=1,DIFF_EQUAL=0;diff_match_patch.Diff=function(a,b){this[0]=a;this[1]=b};diff_match_patch.Diff.prototype.length=2;diff_match_patch.Diff.prototype.toString=function(){return this[0]+","+this[1]};
diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[new diff_match_patch.Diff(DIFF_EQUAL,a)]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);f=this.diff_commonSuffix(a,b);var g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0, diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[new diff_match_patch.Diff(DIFF_EQUAL,a)]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);f=this.diff_commonSuffix(a,b);var g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0,
b.length-f);a=this.diff_compute_(a,b,e,d);c&&a.unshift(new diff_match_patch.Diff(DIFF_EQUAL,c));g&&a.push(new diff_match_patch.Diff(DIFF_EQUAL,g));this.diff_cleanupMerge(a);return a}; b.length-f);a=this.diff_compute_(a,b,e,d);c&&a.unshift(new diff_match_patch.Diff(DIFF_EQUAL,c));g&&a.push(new diff_match_patch.Diff(DIFF_EQUAL,g));this.diff_cleanupMerge(a);return a};

View File

@ -107,6 +107,22 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr>
<td>
<div class="inline-flex">
<span class="label label-warning">Optional</span>
<span>id</span>
</div>
</td>
<td>
<span class="label">String</span>
</td>
<td>
<strong>15 characters string</strong> to store as record ID.
<br />
If not set, it will be auto generated.
</td>
</tr>
{#each collection?.schema as field (field.name)} {#each collection?.schema as field (field.name)}
<tr> <tr>
<td> <td>