Adding upstream version 0.28.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
88f1d47ab6
commit
e28c88ef14
933 changed files with 194711 additions and 0 deletions
499
core/db.go
Normal file
499
core/db.go
Normal file
|
@ -0,0 +1,499 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
const (
|
||||
idColumn string = "id"
|
||||
|
||||
// DefaultIdLength is the default length of the generated model id.
|
||||
DefaultIdLength int = 15
|
||||
|
||||
// DefaultIdAlphabet is the default characters set used for generating the model id.
|
||||
DefaultIdAlphabet string = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
)
|
||||
|
||||
// DefaultIdRegex specifies the default regex pattern for an id value.
|
||||
var DefaultIdRegex = regexp.MustCompile(`^\w+$`)
|
||||
|
||||
// DBExporter defines an interface for custom DB data export.
|
||||
// Usually used as part of [App.Save].
|
||||
type DBExporter interface {
|
||||
// DBExport returns a key-value map with the data to be used when saving the struct in the database.
|
||||
DBExport(app App) (map[string]any, error)
|
||||
}
|
||||
|
||||
// PreValidator defines an optional model interface for registering a
|
||||
// function that will run BEFORE firing the validation hooks (see [App.ValidateWithContext]).
|
||||
type PreValidator interface {
|
||||
// PreValidate defines a function that runs BEFORE the validation hooks.
|
||||
PreValidate(ctx context.Context, app App) error
|
||||
}
|
||||
|
||||
// PostValidator defines an optional model interface for registering a
|
||||
// function that will run AFTER executing the validation hooks (see [App.ValidateWithContext]).
|
||||
type PostValidator interface {
|
||||
// PostValidate defines a function that runs AFTER the successful
|
||||
// execution of the validation hooks.
|
||||
PostValidate(ctx context.Context, app App) error
|
||||
}
|
||||
|
||||
// GenerateDefaultRandomId generates a default random id string
|
||||
// (note: the generated random string is not intended for security purposes).
|
||||
func GenerateDefaultRandomId() string {
|
||||
return security.PseudorandomStringWithAlphabet(DefaultIdLength, DefaultIdAlphabet)
|
||||
}
|
||||
|
||||
// crc32Checksum generates a stringified crc32 checksum from the provided plain string.
|
||||
func crc32Checksum(str string) string {
|
||||
return strconv.FormatInt(int64(crc32.ChecksumIEEE([]byte(str))), 10)
|
||||
}
|
||||
|
||||
// ModelQuery creates a new preconfigured select data.db query with preset
|
||||
// SELECT, FROM and other common fields based on the provided model.
|
||||
func (app *BaseApp) ModelQuery(m Model) *dbx.SelectQuery {
|
||||
return app.modelQuery(app.ConcurrentDB(), m)
|
||||
}
|
||||
|
||||
// AuxModelQuery creates a new preconfigured select auxiliary.db query with preset
|
||||
// SELECT, FROM and other common fields based on the provided model.
|
||||
func (app *BaseApp) AuxModelQuery(m Model) *dbx.SelectQuery {
|
||||
return app.modelQuery(app.AuxConcurrentDB(), m)
|
||||
}
|
||||
|
||||
func (app *BaseApp) modelQuery(db dbx.Builder, m Model) *dbx.SelectQuery {
|
||||
tableName := m.TableName()
|
||||
|
||||
return db.
|
||||
Select("{{" + tableName + "}}.*").
|
||||
From(tableName).
|
||||
WithBuildHook(func(query *dbx.Query) {
|
||||
query.WithExecHook(execLockRetry(app.config.QueryTimeout, defaultMaxLockRetries))
|
||||
})
|
||||
}
|
||||
|
||||
// Delete deletes the specified model from the regular app database.
|
||||
func (app *BaseApp) Delete(model Model) error {
|
||||
return app.DeleteWithContext(context.Background(), model)
|
||||
}
|
||||
|
||||
// Delete deletes the specified model from the regular app database
|
||||
// (the context could be used to limit the query execution).
|
||||
func (app *BaseApp) DeleteWithContext(ctx context.Context, model Model) error {
|
||||
return app.delete(ctx, model, false)
|
||||
}
|
||||
|
||||
// AuxDelete deletes the specified model from the auxiliary database.
|
||||
func (app *BaseApp) AuxDelete(model Model) error {
|
||||
return app.AuxDeleteWithContext(context.Background(), model)
|
||||
}
|
||||
|
||||
// AuxDeleteWithContext deletes the specified model from the auxiliary database
|
||||
// (the context could be used to limit the query execution).
|
||||
func (app *BaseApp) AuxDeleteWithContext(ctx context.Context, model Model) error {
|
||||
return app.delete(ctx, model, true)
|
||||
}
|
||||
|
||||
func (app *BaseApp) delete(ctx context.Context, model Model, isForAuxDB bool) error {
|
||||
event := new(ModelEvent)
|
||||
event.App = app
|
||||
event.Type = ModelEventTypeDelete
|
||||
event.Context = ctx
|
||||
event.Model = model
|
||||
|
||||
deleteErr := app.OnModelDelete().Trigger(event, func(e *ModelEvent) error {
|
||||
pk := cast.ToString(e.Model.LastSavedPK())
|
||||
if pk == "" {
|
||||
return errors.New("the model can be deleted only if it is existing and has a non-empty primary key")
|
||||
}
|
||||
|
||||
// db write
|
||||
return e.App.OnModelDeleteExecute().Trigger(event, func(e *ModelEvent) error {
|
||||
var db dbx.Builder
|
||||
if isForAuxDB {
|
||||
db = e.App.AuxNonconcurrentDB()
|
||||
} else {
|
||||
db = e.App.NonconcurrentDB()
|
||||
}
|
||||
|
||||
return baseLockRetry(func(attempt int) error {
|
||||
_, err := db.Delete(e.Model.TableName(), dbx.HashExp{
|
||||
idColumn: pk,
|
||||
}).WithContext(e.Context).Execute()
|
||||
|
||||
return err
|
||||
}, defaultMaxLockRetries)
|
||||
})
|
||||
})
|
||||
if deleteErr != nil {
|
||||
errEvent := &ModelErrorEvent{ModelEvent: *event, Error: deleteErr}
|
||||
errEvent.App = app // replace with the initial app in case it was changed by the hook
|
||||
hookErr := app.OnModelAfterDeleteError().Trigger(errEvent)
|
||||
if hookErr != nil {
|
||||
return errors.Join(deleteErr, hookErr)
|
||||
}
|
||||
|
||||
return deleteErr
|
||||
}
|
||||
|
||||
if app.txInfo != nil {
|
||||
// execute later after the transaction has completed
|
||||
app.txInfo.OnComplete(func(txErr error) error {
|
||||
if app.txInfo != nil && app.txInfo.parent != nil {
|
||||
event.App = app.txInfo.parent
|
||||
}
|
||||
|
||||
if txErr != nil {
|
||||
return app.OnModelAfterDeleteError().Trigger(&ModelErrorEvent{
|
||||
ModelEvent: *event,
|
||||
Error: txErr,
|
||||
})
|
||||
}
|
||||
|
||||
return app.OnModelAfterDeleteSuccess().Trigger(event)
|
||||
})
|
||||
} else if err := event.App.OnModelAfterDeleteSuccess().Trigger(event); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save validates and saves the specified model into the regular app database.
|
||||
//
|
||||
// If you don't want to run validations, use [App.SaveNoValidate()].
|
||||
func (app *BaseApp) Save(model Model) error {
|
||||
return app.SaveWithContext(context.Background(), model)
|
||||
}
|
||||
|
||||
// SaveWithContext is the same as [App.Save()] but allows specifying a context to limit the db execution.
|
||||
//
|
||||
// If you don't want to run validations, use [App.SaveNoValidateWithContext()].
|
||||
func (app *BaseApp) SaveWithContext(ctx context.Context, model Model) error {
|
||||
return app.save(ctx, model, true, false)
|
||||
}
|
||||
|
||||
// SaveNoValidate saves the specified model into the regular app database without performing validations.
|
||||
//
|
||||
// If you want to also run validations before persisting, use [App.Save()].
|
||||
func (app *BaseApp) SaveNoValidate(model Model) error {
|
||||
return app.SaveNoValidateWithContext(context.Background(), model)
|
||||
}
|
||||
|
||||
// SaveNoValidateWithContext is the same as [App.SaveNoValidate()]
|
||||
// but allows specifying a context to limit the db execution.
|
||||
//
|
||||
// If you want to also run validations before persisting, use [App.SaveWithContext()].
|
||||
func (app *BaseApp) SaveNoValidateWithContext(ctx context.Context, model Model) error {
|
||||
return app.save(ctx, model, false, false)
|
||||
}
|
||||
|
||||
// AuxSave validates and saves the specified model into the auxiliary app database.
|
||||
//
|
||||
// If you don't want to run validations, use [App.AuxSaveNoValidate()].
|
||||
func (app *BaseApp) AuxSave(model Model) error {
|
||||
return app.AuxSaveWithContext(context.Background(), model)
|
||||
}
|
||||
|
||||
// AuxSaveWithContext is the same as [App.AuxSave()] but allows specifying a context to limit the db execution.
|
||||
//
|
||||
// If you don't want to run validations, use [App.AuxSaveNoValidateWithContext()].
|
||||
func (app *BaseApp) AuxSaveWithContext(ctx context.Context, model Model) error {
|
||||
return app.save(ctx, model, true, true)
|
||||
}
|
||||
|
||||
// AuxSaveNoValidate saves the specified model into the auxiliary app database without performing validations.
|
||||
//
|
||||
// If you want to also run validations before persisting, use [App.AuxSave()].
|
||||
func (app *BaseApp) AuxSaveNoValidate(model Model) error {
|
||||
return app.AuxSaveNoValidateWithContext(context.Background(), model)
|
||||
}
|
||||
|
||||
// AuxSaveNoValidateWithContext is the same as [App.AuxSaveNoValidate()]
|
||||
// but allows specifying a context to limit the db execution.
|
||||
//
|
||||
// If you want to also run validations before persisting, use [App.AuxSaveWithContext()].
|
||||
func (app *BaseApp) AuxSaveNoValidateWithContext(ctx context.Context, model Model) error {
|
||||
return app.save(ctx, model, false, true)
|
||||
}
|
||||
|
||||
// Validate triggers the OnModelValidate hook for the specified model.
|
||||
func (app *BaseApp) Validate(model Model) error {
|
||||
return app.ValidateWithContext(context.Background(), model)
|
||||
}
|
||||
|
||||
// ValidateWithContext is the same as Validate but allows specifying the ModelEvent context.
|
||||
func (app *BaseApp) ValidateWithContext(ctx context.Context, model Model) error {
|
||||
if m, ok := model.(PreValidator); ok {
|
||||
if err := m.PreValidate(ctx, app); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
event := new(ModelEvent)
|
||||
event.App = app
|
||||
event.Context = ctx
|
||||
event.Type = ModelEventTypeValidate
|
||||
event.Model = model
|
||||
|
||||
return event.App.OnModelValidate().Trigger(event, func(e *ModelEvent) error {
|
||||
if m, ok := e.Model.(PostValidator); ok {
|
||||
if err := m.PostValidate(ctx, e.App); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
})
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
func (app *BaseApp) save(ctx context.Context, model Model, withValidations bool, isForAuxDB bool) error {
|
||||
if model.IsNew() {
|
||||
return app.create(ctx, model, withValidations, isForAuxDB)
|
||||
}
|
||||
|
||||
return app.update(ctx, model, withValidations, isForAuxDB)
|
||||
}
|
||||
|
||||
func (app *BaseApp) create(ctx context.Context, model Model, withValidations bool, isForAuxDB bool) error {
|
||||
event := new(ModelEvent)
|
||||
event.App = app
|
||||
event.Context = ctx
|
||||
event.Type = ModelEventTypeCreate
|
||||
event.Model = model
|
||||
|
||||
saveErr := app.OnModelCreate().Trigger(event, func(e *ModelEvent) error {
|
||||
// run validations (if any)
|
||||
if withValidations {
|
||||
validateErr := e.App.ValidateWithContext(e.Context, e.Model)
|
||||
if validateErr != nil {
|
||||
return validateErr
|
||||
}
|
||||
}
|
||||
|
||||
// db write
|
||||
return e.App.OnModelCreateExecute().Trigger(event, func(e *ModelEvent) error {
|
||||
var db dbx.Builder
|
||||
if isForAuxDB {
|
||||
db = e.App.AuxNonconcurrentDB()
|
||||
} else {
|
||||
db = e.App.NonconcurrentDB()
|
||||
}
|
||||
|
||||
dbErr := baseLockRetry(func(attempt int) error {
|
||||
if m, ok := e.Model.(DBExporter); ok {
|
||||
data, err := m.DBExport(e.App)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// manually add the id to the data if missing
|
||||
if _, ok := data[idColumn]; !ok {
|
||||
data[idColumn] = e.Model.PK()
|
||||
}
|
||||
|
||||
if cast.ToString(data[idColumn]) == "" {
|
||||
return errors.New("empty primary key is not allowed when using the DBExporter interface")
|
||||
}
|
||||
|
||||
_, err = db.Insert(e.Model.TableName(), data).WithContext(e.Context).Execute()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Model(e.Model).WithContext(e.Context).Insert()
|
||||
}, defaultMaxLockRetries)
|
||||
if dbErr != nil {
|
||||
return dbErr
|
||||
}
|
||||
|
||||
e.Model.MarkAsNotNew()
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if saveErr != nil {
|
||||
event.Model.MarkAsNew() // reset "new" state
|
||||
|
||||
errEvent := &ModelErrorEvent{ModelEvent: *event, Error: saveErr}
|
||||
errEvent.App = app // replace with the initial app in case it was changed by the hook
|
||||
hookErr := app.OnModelAfterCreateError().Trigger(errEvent)
|
||||
if hookErr != nil {
|
||||
return errors.Join(saveErr, hookErr)
|
||||
}
|
||||
|
||||
return saveErr
|
||||
}
|
||||
|
||||
if app.txInfo != nil {
|
||||
// execute later after the transaction has completed
|
||||
app.txInfo.OnComplete(func(txErr error) error {
|
||||
if app.txInfo != nil && app.txInfo.parent != nil {
|
||||
event.App = app.txInfo.parent
|
||||
}
|
||||
|
||||
if txErr != nil {
|
||||
event.Model.MarkAsNew() // reset "new" state
|
||||
|
||||
return app.OnModelAfterCreateError().Trigger(&ModelErrorEvent{
|
||||
ModelEvent: *event,
|
||||
Error: txErr,
|
||||
})
|
||||
}
|
||||
|
||||
return app.OnModelAfterCreateSuccess().Trigger(event)
|
||||
})
|
||||
} else if err := event.App.OnModelAfterCreateSuccess().Trigger(event); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *BaseApp) update(ctx context.Context, model Model, withValidations bool, isForAuxDB bool) error {
|
||||
event := new(ModelEvent)
|
||||
event.App = app
|
||||
event.Context = ctx
|
||||
event.Type = ModelEventTypeUpdate
|
||||
event.Model = model
|
||||
|
||||
saveErr := app.OnModelUpdate().Trigger(event, func(e *ModelEvent) error {
|
||||
// run validations (if any)
|
||||
if withValidations {
|
||||
validateErr := e.App.ValidateWithContext(e.Context, e.Model)
|
||||
if validateErr != nil {
|
||||
return validateErr
|
||||
}
|
||||
}
|
||||
|
||||
// db write
|
||||
return e.App.OnModelUpdateExecute().Trigger(event, func(e *ModelEvent) error {
|
||||
var db dbx.Builder
|
||||
if isForAuxDB {
|
||||
db = e.App.AuxNonconcurrentDB()
|
||||
} else {
|
||||
db = e.App.NonconcurrentDB()
|
||||
}
|
||||
|
||||
return baseLockRetry(func(attempt int) error {
|
||||
if m, ok := e.Model.(DBExporter); ok {
|
||||
data, err := m.DBExport(e.App)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// note: for now disallow primary key change for consistency with dbx.ModelQuery.Update()
|
||||
if data[idColumn] != e.Model.LastSavedPK() {
|
||||
return errors.New("primary key change is not allowed")
|
||||
}
|
||||
|
||||
_, err = db.Update(e.Model.TableName(), data, dbx.HashExp{
|
||||
idColumn: e.Model.LastSavedPK(),
|
||||
}).WithContext(e.Context).Execute()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return db.Model(e.Model).WithContext(e.Context).Update()
|
||||
}, defaultMaxLockRetries)
|
||||
})
|
||||
})
|
||||
if saveErr != nil {
|
||||
errEvent := &ModelErrorEvent{ModelEvent: *event, Error: saveErr}
|
||||
errEvent.App = app // replace with the initial app in case it was changed by the hook
|
||||
hookErr := app.OnModelAfterUpdateError().Trigger(errEvent)
|
||||
if hookErr != nil {
|
||||
return errors.Join(saveErr, hookErr)
|
||||
}
|
||||
|
||||
return saveErr
|
||||
}
|
||||
|
||||
if app.txInfo != nil {
|
||||
// execute later after the transaction has completed
|
||||
app.txInfo.OnComplete(func(txErr error) error {
|
||||
if app.txInfo != nil && app.txInfo.parent != nil {
|
||||
event.App = app.txInfo.parent
|
||||
}
|
||||
|
||||
if txErr != nil {
|
||||
return app.OnModelAfterUpdateError().Trigger(&ModelErrorEvent{
|
||||
ModelEvent: *event,
|
||||
Error: txErr,
|
||||
})
|
||||
}
|
||||
|
||||
return app.OnModelAfterUpdateSuccess().Trigger(event)
|
||||
})
|
||||
} else if err := event.App.OnModelAfterUpdateSuccess().Trigger(event); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCollectionId(app App, optTypes ...string) validation.RuleFunc {
|
||||
return func(value any) error {
|
||||
id, _ := value.(string)
|
||||
if id == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
collection := &Collection{}
|
||||
if err := app.ModelQuery(collection).Model(id, collection); err != nil {
|
||||
return validation.NewError("validation_invalid_collection_id", "Missing or invalid collection.")
|
||||
}
|
||||
|
||||
if len(optTypes) > 0 && !slices.Contains(optTypes, collection.Type) {
|
||||
return validation.NewError(
|
||||
"validation_invalid_collection_type",
|
||||
fmt.Sprintf("Invalid collection type - must be %s.", strings.Join(optTypes, ", ")),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateRecordId(app App, collectionNameOrId string) validation.RuleFunc {
|
||||
return func(value any) error {
|
||||
id, _ := value.(string)
|
||||
if id == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
collection, err := app.FindCachedCollectionByNameOrId(collectionNameOrId)
|
||||
if err != nil {
|
||||
return validation.NewError("validation_invalid_collection", "Missing or invalid collection.")
|
||||
}
|
||||
|
||||
var exists int
|
||||
|
||||
rowErr := app.ConcurrentDB().Select("(1)").
|
||||
From(collection.Name).
|
||||
AndWhere(dbx.HashExp{"id": id}).
|
||||
Limit(1).
|
||||
Row(&exists)
|
||||
|
||||
if rowErr != nil || exists == 0 {
|
||||
return validation.NewError("validation_invalid_record", "Missing or invalid record.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue