mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-01-27 23:46:18 +02:00
1410 lines
49 KiB
Go
1410 lines
49 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/pocketbase/dbx"
|
|
"github.com/pocketbase/pocketbase/tools/cron"
|
|
"github.com/pocketbase/pocketbase/tools/filesystem"
|
|
"github.com/pocketbase/pocketbase/tools/hook"
|
|
"github.com/pocketbase/pocketbase/tools/logger"
|
|
"github.com/pocketbase/pocketbase/tools/mailer"
|
|
"github.com/pocketbase/pocketbase/tools/routine"
|
|
"github.com/pocketbase/pocketbase/tools/store"
|
|
"github.com/pocketbase/pocketbase/tools/subscriptions"
|
|
"github.com/pocketbase/pocketbase/tools/types"
|
|
)
|
|
|
|
const (
|
|
DefaultDataMaxOpenConns int = 120
|
|
DefaultDataMaxIdleConns int = 15
|
|
DefaultAuxMaxOpenConns int = 20
|
|
DefaultAuxMaxIdleConns int = 3
|
|
DefaultQueryTimeout time.Duration = 30 * time.Second
|
|
|
|
LocalStorageDirName string = "storage"
|
|
LocalBackupsDirName string = "backups"
|
|
LocalTempDirName string = ".pb_temp_to_delete" // temp pb_data sub directory that will be deleted on each app.Bootstrap()
|
|
LocalAutocertCacheDirName string = ".autocert_cache"
|
|
)
|
|
|
|
// FilesManager defines an interface with common methods that files manager models should implement.
|
|
type FilesManager interface {
|
|
// BaseFilesPath returns the storage dir path used by the interface instance.
|
|
BaseFilesPath() string
|
|
}
|
|
|
|
// DBConnectFunc defines a database connection initialization function.
|
|
type DBConnectFunc func(dbPath string) (*dbx.DB, error)
|
|
|
|
// BaseAppConfig defines a BaseApp configuration option
|
|
type BaseAppConfig struct {
|
|
DBConnect DBConnectFunc
|
|
DataDir string
|
|
EncryptionEnv string
|
|
QueryTimeout time.Duration
|
|
DataMaxOpenConns int
|
|
DataMaxIdleConns int
|
|
AuxMaxOpenConns int
|
|
AuxMaxIdleConns int
|
|
IsDev bool
|
|
}
|
|
|
|
// ensures that the BaseApp implements the App interface.
|
|
var _ App = (*BaseApp)(nil)
|
|
|
|
// BaseApp implements core.App and defines the base PocketBase app structure.
|
|
type BaseApp struct {
|
|
config *BaseAppConfig
|
|
txInfo *txAppInfo
|
|
store *store.Store[any]
|
|
cron *cron.Cron
|
|
settings *Settings
|
|
subscriptionsBroker *subscriptions.Broker
|
|
logger *slog.Logger
|
|
concurrentDB dbx.Builder
|
|
nonconcurrentDB dbx.Builder
|
|
auxConcurrentDB dbx.Builder
|
|
auxNonconcurrentDB dbx.Builder
|
|
|
|
// app event hooks
|
|
onBootstrap *hook.Hook[*BootstrapEvent]
|
|
onServe *hook.Hook[*ServeEvent]
|
|
onTerminate *hook.Hook[*TerminateEvent]
|
|
onBackupCreate *hook.Hook[*BackupEvent]
|
|
onBackupRestore *hook.Hook[*BackupEvent]
|
|
|
|
// db model hooks
|
|
onModelValidate *hook.Hook[*ModelEvent]
|
|
onModelCreate *hook.Hook[*ModelEvent]
|
|
onModelCreateExecute *hook.Hook[*ModelEvent]
|
|
onModelAfterCreateSuccess *hook.Hook[*ModelEvent]
|
|
onModelAfterCreateError *hook.Hook[*ModelErrorEvent]
|
|
onModelUpdate *hook.Hook[*ModelEvent]
|
|
onModelUpdateWrite *hook.Hook[*ModelEvent]
|
|
onModelAfterUpdateSuccess *hook.Hook[*ModelEvent]
|
|
onModelAfterUpdateError *hook.Hook[*ModelErrorEvent]
|
|
onModelDelete *hook.Hook[*ModelEvent]
|
|
onModelDeleteExecute *hook.Hook[*ModelEvent]
|
|
onModelAfterDeleteSuccess *hook.Hook[*ModelEvent]
|
|
onModelAfterDeleteError *hook.Hook[*ModelErrorEvent]
|
|
|
|
// db record hooks
|
|
onRecordEnrich *hook.Hook[*RecordEnrichEvent]
|
|
onRecordValidate *hook.Hook[*RecordEvent]
|
|
onRecordCreate *hook.Hook[*RecordEvent]
|
|
onRecordCreateExecute *hook.Hook[*RecordEvent]
|
|
onRecordAfterCreateSuccess *hook.Hook[*RecordEvent]
|
|
onRecordAfterCreateError *hook.Hook[*RecordErrorEvent]
|
|
onRecordUpdate *hook.Hook[*RecordEvent]
|
|
onRecordUpdateExecute *hook.Hook[*RecordEvent]
|
|
onRecordAfterUpdateSuccess *hook.Hook[*RecordEvent]
|
|
onRecordAfterUpdateError *hook.Hook[*RecordErrorEvent]
|
|
onRecordDelete *hook.Hook[*RecordEvent]
|
|
onRecordDeleteExecute *hook.Hook[*RecordEvent]
|
|
onRecordAfterDeleteSuccess *hook.Hook[*RecordEvent]
|
|
onRecordAfterDeleteError *hook.Hook[*RecordErrorEvent]
|
|
|
|
// db collection hooks
|
|
onCollectionValidate *hook.Hook[*CollectionEvent]
|
|
onCollectionCreate *hook.Hook[*CollectionEvent]
|
|
onCollectionCreateExecute *hook.Hook[*CollectionEvent]
|
|
onCollectionAfterCreateSuccess *hook.Hook[*CollectionEvent]
|
|
onCollectionAfterCreateError *hook.Hook[*CollectionErrorEvent]
|
|
onCollectionUpdate *hook.Hook[*CollectionEvent]
|
|
onCollectionUpdateExecute *hook.Hook[*CollectionEvent]
|
|
onCollectionAfterUpdateSuccess *hook.Hook[*CollectionEvent]
|
|
onCollectionAfterUpdateError *hook.Hook[*CollectionErrorEvent]
|
|
onCollectionDelete *hook.Hook[*CollectionEvent]
|
|
onCollectionDeleteExecute *hook.Hook[*CollectionEvent]
|
|
onCollectionAfterDeleteSuccess *hook.Hook[*CollectionEvent]
|
|
onCollectionAfterDeleteError *hook.Hook[*CollectionErrorEvent]
|
|
|
|
// mailer event hooks
|
|
onMailerSend *hook.Hook[*MailerEvent]
|
|
onMailerRecordPasswordResetSend *hook.Hook[*MailerRecordEvent]
|
|
onMailerRecordVerificationSend *hook.Hook[*MailerRecordEvent]
|
|
onMailerRecordEmailChangeSend *hook.Hook[*MailerRecordEvent]
|
|
onMailerRecordOTPSend *hook.Hook[*MailerRecordEvent]
|
|
onMailerRecordAuthAlertSend *hook.Hook[*MailerRecordEvent]
|
|
|
|
// realtime api event hooks
|
|
onRealtimeConnectRequest *hook.Hook[*RealtimeConnectRequestEvent]
|
|
onRealtimeMessageSend *hook.Hook[*RealtimeMessageEvent]
|
|
onRealtimeSubscribeRequest *hook.Hook[*RealtimeSubscribeRequestEvent]
|
|
|
|
// settings event hooks
|
|
onSettingsListRequest *hook.Hook[*SettingsListRequestEvent]
|
|
onSettingsUpdateRequest *hook.Hook[*SettingsUpdateRequestEvent]
|
|
onSettingsReload *hook.Hook[*SettingsReloadEvent]
|
|
|
|
// file api event hooks
|
|
onFileDownloadRequest *hook.Hook[*FileDownloadRequestEvent]
|
|
onFileTokenRequest *hook.Hook[*FileTokenRequestEvent]
|
|
|
|
// record auth API event hooks
|
|
onRecordAuthRequest *hook.Hook[*RecordAuthRequestEvent]
|
|
onRecordAuthWithPasswordRequest *hook.Hook[*RecordAuthWithPasswordRequestEvent]
|
|
onRecordAuthWithOAuth2Request *hook.Hook[*RecordAuthWithOAuth2RequestEvent]
|
|
onRecordAuthRefreshRequest *hook.Hook[*RecordAuthRefreshRequestEvent]
|
|
onRecordRequestPasswordResetRequest *hook.Hook[*RecordRequestPasswordResetRequestEvent]
|
|
onRecordConfirmPasswordResetRequest *hook.Hook[*RecordConfirmPasswordResetRequestEvent]
|
|
onRecordRequestVerificationRequest *hook.Hook[*RecordRequestVerificationRequestEvent]
|
|
onRecordConfirmVerificationRequest *hook.Hook[*RecordConfirmVerificationRequestEvent]
|
|
onRecordRequestEmailChangeRequest *hook.Hook[*RecordRequestEmailChangeRequestEvent]
|
|
onRecordConfirmEmailChangeRequest *hook.Hook[*RecordConfirmEmailChangeRequestEvent]
|
|
onRecordRequestOTPRequest *hook.Hook[*RecordCreateOTPRequestEvent]
|
|
onRecordAuthWithOTPRequest *hook.Hook[*RecordAuthWithOTPRequestEvent]
|
|
|
|
// record crud API event hooks
|
|
onRecordsListRequest *hook.Hook[*RecordsListRequestEvent]
|
|
onRecordViewRequest *hook.Hook[*RecordRequestEvent]
|
|
onRecordCreateRequest *hook.Hook[*RecordRequestEvent]
|
|
onRecordUpdateRequest *hook.Hook[*RecordRequestEvent]
|
|
onRecordDeleteRequest *hook.Hook[*RecordRequestEvent]
|
|
|
|
// collection API event hooks
|
|
onCollectionsListRequest *hook.Hook[*CollectionsListRequestEvent]
|
|
onCollectionViewRequest *hook.Hook[*CollectionRequestEvent]
|
|
onCollectionCreateRequest *hook.Hook[*CollectionRequestEvent]
|
|
onCollectionUpdateRequest *hook.Hook[*CollectionRequestEvent]
|
|
onCollectionDeleteRequest *hook.Hook[*CollectionRequestEvent]
|
|
onCollectionsImportRequest *hook.Hook[*CollectionsImportRequestEvent]
|
|
|
|
onBatchRequest *hook.Hook[*BatchRequestEvent]
|
|
}
|
|
|
|
// NewBaseApp creates and returns a new BaseApp instance
|
|
// configured with the provided arguments.
|
|
//
|
|
// To initialize the app, you need to call `app.Bootstrap()`.
|
|
func NewBaseApp(config BaseAppConfig) *BaseApp {
|
|
app := &BaseApp{
|
|
settings: newDefaultSettings(),
|
|
store: store.New[any](nil),
|
|
cron: cron.New(),
|
|
subscriptionsBroker: subscriptions.NewBroker(),
|
|
config: &config,
|
|
}
|
|
|
|
// apply config defaults
|
|
if app.config.DBConnect == nil {
|
|
app.config.DBConnect = dbConnect
|
|
}
|
|
if app.config.DataMaxOpenConns <= 0 {
|
|
app.config.DataMaxOpenConns = DefaultDataMaxOpenConns
|
|
}
|
|
if app.config.DataMaxIdleConns <= 0 {
|
|
app.config.DataMaxIdleConns = DefaultDataMaxIdleConns
|
|
}
|
|
if app.config.AuxMaxOpenConns <= 0 {
|
|
app.config.AuxMaxOpenConns = DefaultAuxMaxOpenConns
|
|
}
|
|
if app.config.AuxMaxIdleConns <= 0 {
|
|
app.config.AuxMaxIdleConns = DefaultAuxMaxIdleConns
|
|
}
|
|
if app.config.QueryTimeout <= 0 {
|
|
app.config.QueryTimeout = DefaultQueryTimeout
|
|
}
|
|
|
|
app.initHooks()
|
|
app.registerBaseHooks()
|
|
|
|
return app
|
|
}
|
|
|
|
// initHooks initializes all app hook handlers.
|
|
func (app *BaseApp) initHooks() {
|
|
// app event hooks
|
|
app.onBootstrap = &hook.Hook[*BootstrapEvent]{}
|
|
app.onServe = &hook.Hook[*ServeEvent]{}
|
|
app.onTerminate = &hook.Hook[*TerminateEvent]{}
|
|
app.onBackupCreate = &hook.Hook[*BackupEvent]{}
|
|
app.onBackupRestore = &hook.Hook[*BackupEvent]{}
|
|
|
|
// db model hooks
|
|
app.onModelValidate = &hook.Hook[*ModelEvent]{}
|
|
app.onModelCreate = &hook.Hook[*ModelEvent]{}
|
|
app.onModelCreateExecute = &hook.Hook[*ModelEvent]{}
|
|
app.onModelAfterCreateSuccess = &hook.Hook[*ModelEvent]{}
|
|
app.onModelAfterCreateError = &hook.Hook[*ModelErrorEvent]{}
|
|
app.onModelUpdate = &hook.Hook[*ModelEvent]{}
|
|
app.onModelUpdateWrite = &hook.Hook[*ModelEvent]{}
|
|
app.onModelAfterUpdateSuccess = &hook.Hook[*ModelEvent]{}
|
|
app.onModelAfterUpdateError = &hook.Hook[*ModelErrorEvent]{}
|
|
app.onModelDelete = &hook.Hook[*ModelEvent]{}
|
|
app.onModelDeleteExecute = &hook.Hook[*ModelEvent]{}
|
|
app.onModelAfterDeleteSuccess = &hook.Hook[*ModelEvent]{}
|
|
app.onModelAfterDeleteError = &hook.Hook[*ModelErrorEvent]{}
|
|
|
|
// db record hooks
|
|
app.onRecordEnrich = &hook.Hook[*RecordEnrichEvent]{}
|
|
app.onRecordValidate = &hook.Hook[*RecordEvent]{}
|
|
app.onRecordCreate = &hook.Hook[*RecordEvent]{}
|
|
app.onRecordCreateExecute = &hook.Hook[*RecordEvent]{}
|
|
app.onRecordAfterCreateSuccess = &hook.Hook[*RecordEvent]{}
|
|
app.onRecordAfterCreateError = &hook.Hook[*RecordErrorEvent]{}
|
|
app.onRecordUpdate = &hook.Hook[*RecordEvent]{}
|
|
app.onRecordUpdateExecute = &hook.Hook[*RecordEvent]{}
|
|
app.onRecordAfterUpdateSuccess = &hook.Hook[*RecordEvent]{}
|
|
app.onRecordAfterUpdateError = &hook.Hook[*RecordErrorEvent]{}
|
|
app.onRecordDelete = &hook.Hook[*RecordEvent]{}
|
|
app.onRecordDeleteExecute = &hook.Hook[*RecordEvent]{}
|
|
app.onRecordAfterDeleteSuccess = &hook.Hook[*RecordEvent]{}
|
|
app.onRecordAfterDeleteError = &hook.Hook[*RecordErrorEvent]{}
|
|
|
|
// db collection hooks
|
|
app.onCollectionValidate = &hook.Hook[*CollectionEvent]{}
|
|
app.onCollectionCreate = &hook.Hook[*CollectionEvent]{}
|
|
app.onCollectionCreateExecute = &hook.Hook[*CollectionEvent]{}
|
|
app.onCollectionAfterCreateSuccess = &hook.Hook[*CollectionEvent]{}
|
|
app.onCollectionAfterCreateError = &hook.Hook[*CollectionErrorEvent]{}
|
|
app.onCollectionUpdate = &hook.Hook[*CollectionEvent]{}
|
|
app.onCollectionUpdateExecute = &hook.Hook[*CollectionEvent]{}
|
|
app.onCollectionAfterUpdateSuccess = &hook.Hook[*CollectionEvent]{}
|
|
app.onCollectionAfterUpdateError = &hook.Hook[*CollectionErrorEvent]{}
|
|
app.onCollectionDelete = &hook.Hook[*CollectionEvent]{}
|
|
app.onCollectionAfterDeleteSuccess = &hook.Hook[*CollectionEvent]{}
|
|
app.onCollectionDeleteExecute = &hook.Hook[*CollectionEvent]{}
|
|
app.onCollectionAfterDeleteError = &hook.Hook[*CollectionErrorEvent]{}
|
|
|
|
// mailer event hooks
|
|
app.onMailerSend = &hook.Hook[*MailerEvent]{}
|
|
app.onMailerRecordPasswordResetSend = &hook.Hook[*MailerRecordEvent]{}
|
|
app.onMailerRecordVerificationSend = &hook.Hook[*MailerRecordEvent]{}
|
|
app.onMailerRecordEmailChangeSend = &hook.Hook[*MailerRecordEvent]{}
|
|
app.onMailerRecordOTPSend = &hook.Hook[*MailerRecordEvent]{}
|
|
app.onMailerRecordAuthAlertSend = &hook.Hook[*MailerRecordEvent]{}
|
|
|
|
// realtime API event hooks
|
|
app.onRealtimeConnectRequest = &hook.Hook[*RealtimeConnectRequestEvent]{}
|
|
app.onRealtimeMessageSend = &hook.Hook[*RealtimeMessageEvent]{}
|
|
app.onRealtimeSubscribeRequest = &hook.Hook[*RealtimeSubscribeRequestEvent]{}
|
|
|
|
// settings event hooks
|
|
app.onSettingsListRequest = &hook.Hook[*SettingsListRequestEvent]{}
|
|
app.onSettingsUpdateRequest = &hook.Hook[*SettingsUpdateRequestEvent]{}
|
|
app.onSettingsReload = &hook.Hook[*SettingsReloadEvent]{}
|
|
|
|
// file API event hooks
|
|
app.onFileDownloadRequest = &hook.Hook[*FileDownloadRequestEvent]{}
|
|
app.onFileTokenRequest = &hook.Hook[*FileTokenRequestEvent]{}
|
|
|
|
// record auth API event hooks
|
|
app.onRecordAuthRequest = &hook.Hook[*RecordAuthRequestEvent]{}
|
|
app.onRecordAuthWithPasswordRequest = &hook.Hook[*RecordAuthWithPasswordRequestEvent]{}
|
|
app.onRecordAuthWithOAuth2Request = &hook.Hook[*RecordAuthWithOAuth2RequestEvent]{}
|
|
app.onRecordAuthRefreshRequest = &hook.Hook[*RecordAuthRefreshRequestEvent]{}
|
|
app.onRecordRequestPasswordResetRequest = &hook.Hook[*RecordRequestPasswordResetRequestEvent]{}
|
|
app.onRecordConfirmPasswordResetRequest = &hook.Hook[*RecordConfirmPasswordResetRequestEvent]{}
|
|
app.onRecordRequestVerificationRequest = &hook.Hook[*RecordRequestVerificationRequestEvent]{}
|
|
app.onRecordConfirmVerificationRequest = &hook.Hook[*RecordConfirmVerificationRequestEvent]{}
|
|
app.onRecordRequestEmailChangeRequest = &hook.Hook[*RecordRequestEmailChangeRequestEvent]{}
|
|
app.onRecordConfirmEmailChangeRequest = &hook.Hook[*RecordConfirmEmailChangeRequestEvent]{}
|
|
app.onRecordRequestOTPRequest = &hook.Hook[*RecordCreateOTPRequestEvent]{}
|
|
app.onRecordAuthWithOTPRequest = &hook.Hook[*RecordAuthWithOTPRequestEvent]{}
|
|
|
|
// record crud API event hooks
|
|
app.onRecordsListRequest = &hook.Hook[*RecordsListRequestEvent]{}
|
|
app.onRecordViewRequest = &hook.Hook[*RecordRequestEvent]{}
|
|
app.onRecordCreateRequest = &hook.Hook[*RecordRequestEvent]{}
|
|
app.onRecordUpdateRequest = &hook.Hook[*RecordRequestEvent]{}
|
|
app.onRecordDeleteRequest = &hook.Hook[*RecordRequestEvent]{}
|
|
|
|
// collection API event hooks
|
|
app.onCollectionsListRequest = &hook.Hook[*CollectionsListRequestEvent]{}
|
|
app.onCollectionViewRequest = &hook.Hook[*CollectionRequestEvent]{}
|
|
app.onCollectionCreateRequest = &hook.Hook[*CollectionRequestEvent]{}
|
|
app.onCollectionUpdateRequest = &hook.Hook[*CollectionRequestEvent]{}
|
|
app.onCollectionDeleteRequest = &hook.Hook[*CollectionRequestEvent]{}
|
|
app.onCollectionsImportRequest = &hook.Hook[*CollectionsImportRequestEvent]{}
|
|
|
|
app.onBatchRequest = &hook.Hook[*BatchRequestEvent]{}
|
|
}
|
|
|
|
// UnsafeWithoutHooks returns a shallow copy of the current app WITHOUT any registered hooks.
|
|
//
|
|
// NB! Note that using the returned app instance may cause data integrity errors
|
|
// since the Record validations and data normalizations (including files uploads)
|
|
// rely on the app hooks to work.
|
|
//
|
|
// @todo consider caching the created instance?
|
|
func (app *BaseApp) UnsafeWithoutHooks() App {
|
|
clone := *app
|
|
|
|
// reset all hook handlers
|
|
clone.initHooks()
|
|
|
|
return &clone
|
|
}
|
|
|
|
// Logger returns the default app logger.
|
|
//
|
|
// If the application is not bootstrapped yet, fallbacks to slog.Default().
|
|
func (app *BaseApp) Logger() *slog.Logger {
|
|
if app.logger == nil {
|
|
return slog.Default()
|
|
}
|
|
|
|
return app.logger
|
|
}
|
|
|
|
// IsTransactional checks if the current app instance is part of a transaction.
|
|
func (app *BaseApp) IsTransactional() bool {
|
|
return app.txInfo != nil
|
|
}
|
|
|
|
// IsBootstrapped checks if the application was initialized
|
|
// (aka. whether Bootstrap() was called).
|
|
func (app *BaseApp) IsBootstrapped() bool {
|
|
return app.concurrentDB != nil && app.auxConcurrentDB != nil
|
|
}
|
|
|
|
// Bootstrap initializes the application
|
|
// (aka. create data dir, open db connections, load settings, etc.).
|
|
//
|
|
// It will call ResetBootstrapState() if the application was already bootstrapped.
|
|
func (app *BaseApp) Bootstrap() error {
|
|
event := &BootstrapEvent{}
|
|
event.App = app
|
|
|
|
err := app.OnBootstrap().Trigger(event, func(e *BootstrapEvent) error {
|
|
// clear resources of previous core state (if any)
|
|
if err := app.ResetBootstrapState(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// ensure that data dir exist
|
|
if err := os.MkdirAll(app.DataDir(), os.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := app.initDataDB(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := app.initAuxDB(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := app.initLogger(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := app.RunSystemMigrations(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := app.ReloadCachedCollections(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := app.ReloadSettings(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// try to cleanup the pb_data temp directory (if any)
|
|
_ = os.RemoveAll(filepath.Join(app.DataDir(), LocalTempDirName))
|
|
|
|
return nil
|
|
})
|
|
|
|
// add a more user friendly message in case users forgot to call
|
|
// e.Next() as part of their bootstrap hook
|
|
if err == nil && !app.IsBootstrapped() {
|
|
app.Logger().Warn("OnBootstrap hook didn't fail but the app is still not bootstrapped - maybe missing e.Next()?")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
type closer interface {
|
|
Close() error
|
|
}
|
|
|
|
// ResetBootstrapState releases the initialized core app resources
|
|
// (closing db connections, stopping cron ticker, etc.).
|
|
func (app *BaseApp) ResetBootstrapState() error {
|
|
app.Cron().Stop()
|
|
|
|
var errs []error
|
|
|
|
dbs := []*dbx.Builder{
|
|
&app.concurrentDB,
|
|
&app.nonconcurrentDB,
|
|
&app.auxConcurrentDB,
|
|
&app.auxNonconcurrentDB,
|
|
}
|
|
|
|
for _, db := range dbs {
|
|
if db == nil {
|
|
continue
|
|
}
|
|
if v, ok := (*db).(closer); ok {
|
|
if err := v.Close(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
*db = nil
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DB returns the default app data db instance (pb_data/data.db).
|
|
func (app *BaseApp) DB() dbx.Builder {
|
|
return app.concurrentDB
|
|
}
|
|
|
|
// NonconcurrentDB returns the nonconcurrent app data db instance (pb_data/data.db).
|
|
//
|
|
// The returned db instance is limited only to a single open connection,
|
|
// meaning that it can process only 1 db operation at a time (other operations will be queued up).
|
|
//
|
|
// This method is used mainly internally and in the tests to execute write
|
|
// (save/delete) db operations as it helps with minimizing the SQLITE_BUSY errors.
|
|
//
|
|
// For the majority of cases you would want to use the regular DB() method
|
|
// since it allows concurrent db read operations.
|
|
//
|
|
// In a transaction the ConcurrentDB() and NonconcurrentDB() refer to the same *dbx.TX instance.
|
|
func (app *BaseApp) NonconcurrentDB() dbx.Builder {
|
|
return app.nonconcurrentDB
|
|
}
|
|
|
|
// AuxDB returns the default app auxiliary db instance (pb_data/auxiliary.db).
|
|
func (app *BaseApp) AuxDB() dbx.Builder {
|
|
return app.auxConcurrentDB
|
|
}
|
|
|
|
// AuxNonconcurrentDB returns the nonconcurrent app auxiliary db instance (pb_data/auxiliary.db).
|
|
//
|
|
// The returned db instance is limited only to a single open connection,
|
|
// meaning that it can process only 1 db operation at a time (other operations will be queued up).
|
|
//
|
|
// This method is used mainly internally and in the tests to execute write
|
|
// (save/delete) db operations as it helps with minimizing the SQLITE_BUSY errors.
|
|
//
|
|
// For the majority of cases you would want to use the regular DB() method
|
|
// since it allows concurrent db read operations.
|
|
//
|
|
// In a transaction the AuxNonconcurrentDB() and AuxNonconcurrentDB() refer to the same *dbx.TX instance.
|
|
func (app *BaseApp) AuxNonconcurrentDB() dbx.Builder {
|
|
return app.auxNonconcurrentDB
|
|
}
|
|
|
|
// DataDir returns the app data directory path.
|
|
func (app *BaseApp) DataDir() string {
|
|
return app.config.DataDir
|
|
}
|
|
|
|
// EncryptionEnv returns the name of the app secret env key
|
|
// (currently used primarily for optional settings encryption but this may change in the future).
|
|
func (app *BaseApp) EncryptionEnv() string {
|
|
return app.config.EncryptionEnv
|
|
}
|
|
|
|
// IsDev returns whether the app is in dev mode.
|
|
//
|
|
// When enabled logs, executed sql statements, etc. are printed to the stderr.
|
|
func (app *BaseApp) IsDev() bool {
|
|
return app.config.IsDev
|
|
}
|
|
|
|
// Settings returns the loaded app settings.
|
|
func (app *BaseApp) Settings() *Settings {
|
|
return app.settings
|
|
}
|
|
|
|
// Store returns the app runtime store.
|
|
func (app *BaseApp) Store() *store.Store[any] {
|
|
return app.store
|
|
}
|
|
|
|
// Cron returns the app cron instance.
|
|
func (app *BaseApp) Cron() *cron.Cron {
|
|
return app.cron
|
|
}
|
|
|
|
// SubscriptionsBroker returns the app realtime subscriptions broker instance.
|
|
func (app *BaseApp) SubscriptionsBroker() *subscriptions.Broker {
|
|
return app.subscriptionsBroker
|
|
}
|
|
|
|
// NewMailClient creates and returns a new SMTP or Sendmail client
|
|
// based on the current app settings.
|
|
func (app *BaseApp) NewMailClient() mailer.Mailer {
|
|
var client mailer.Mailer
|
|
|
|
// init mailer client
|
|
if app.Settings().SMTP.Enabled {
|
|
client = &mailer.SMTPClient{
|
|
Host: app.Settings().SMTP.Host,
|
|
Port: app.Settings().SMTP.Port,
|
|
Username: app.Settings().SMTP.Username,
|
|
Password: app.Settings().SMTP.Password,
|
|
TLS: app.Settings().SMTP.TLS,
|
|
AuthMethod: app.Settings().SMTP.AuthMethod,
|
|
LocalName: app.Settings().SMTP.LocalName,
|
|
}
|
|
} else {
|
|
client = &mailer.Sendmail{}
|
|
}
|
|
|
|
// register the app level hook
|
|
if h, ok := client.(mailer.SendInterceptor); ok {
|
|
h.OnSend().Bind(&hook.Handler[*mailer.SendEvent]{
|
|
Id: "__pbMailerOnSend__",
|
|
Func: func(e *mailer.SendEvent) error {
|
|
appEvent := new(MailerEvent)
|
|
appEvent.App = app
|
|
appEvent.Mailer = client
|
|
appEvent.Message = e.Message
|
|
|
|
return app.OnMailerSend().Trigger(appEvent, func(ae *MailerEvent) error {
|
|
e.Message = ae.Message
|
|
|
|
// print the mail in the console to assist with the debugging
|
|
if app.IsDev() {
|
|
logDate := new(strings.Builder)
|
|
log.New(logDate, "", log.LstdFlags).Print()
|
|
|
|
mailLog := new(strings.Builder)
|
|
mailLog.WriteString(strings.TrimSpace(logDate.String()))
|
|
mailLog.WriteString(" Mail sent\n")
|
|
fmt.Fprintf(mailLog, "├─ From: %v\n", ae.Message.From)
|
|
fmt.Fprintf(mailLog, "├─ To: %v\n", ae.Message.To)
|
|
fmt.Fprintf(mailLog, "├─ Cc: %v\n", ae.Message.Cc)
|
|
fmt.Fprintf(mailLog, "├─ Bcc: %v\n", ae.Message.Bcc)
|
|
fmt.Fprintf(mailLog, "├─ Subject: %v\n", ae.Message.Subject)
|
|
|
|
if len(ae.Message.Attachments) > 0 {
|
|
attachmentKeys := make([]string, 0, len(ae.Message.Attachments))
|
|
for k := range ae.Message.Attachments {
|
|
attachmentKeys = append(attachmentKeys, k)
|
|
}
|
|
fmt.Fprintf(mailLog, "├─ Attachments: %v\n", attachmentKeys)
|
|
}
|
|
|
|
const indentation = " "
|
|
if ae.Message.Text != "" {
|
|
textParts := strings.Split(strings.TrimSpace(ae.Message.Text), "\n")
|
|
textIndented := indentation + strings.Join(textParts, "\n"+indentation)
|
|
fmt.Fprintf(mailLog, "└─ Text:\n%s", textIndented)
|
|
} else {
|
|
htmlParts := strings.Split(strings.TrimSpace(ae.Message.HTML), "\n")
|
|
htmlIndented := indentation + strings.Join(htmlParts, "\n"+indentation)
|
|
fmt.Fprintf(mailLog, "└─ HTML:\n%s", htmlIndented)
|
|
}
|
|
|
|
color.HiBlack("%s", mailLog.String())
|
|
}
|
|
|
|
// send the email with the new mailer in case it was replaced
|
|
if client != ae.Mailer {
|
|
return ae.Mailer.Send(e.Message)
|
|
}
|
|
|
|
return e.Next()
|
|
})
|
|
},
|
|
})
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
// NewFilesystem creates a new local or S3 filesystem instance
|
|
// for managing regular app files (ex. record uploads)
|
|
// based on the current app settings.
|
|
//
|
|
// NB! Make sure to call Close() on the returned result
|
|
// after you are done working with it.
|
|
func (app *BaseApp) NewFilesystem() (*filesystem.System, error) {
|
|
if app.settings != nil && app.settings.S3.Enabled {
|
|
return filesystem.NewS3(
|
|
app.settings.S3.Bucket,
|
|
app.settings.S3.Region,
|
|
app.settings.S3.Endpoint,
|
|
app.settings.S3.AccessKey,
|
|
app.settings.S3.Secret,
|
|
app.settings.S3.ForcePathStyle,
|
|
)
|
|
}
|
|
|
|
// fallback to local filesystem
|
|
return filesystem.NewLocal(filepath.Join(app.DataDir(), LocalStorageDirName))
|
|
}
|
|
|
|
// NewFilesystem creates a new local or S3 filesystem instance
|
|
// for managing app backups based on the current app settings.
|
|
//
|
|
// NB! Make sure to call Close() on the returned result
|
|
// after you are done working with it.
|
|
func (app *BaseApp) NewBackupsFilesystem() (*filesystem.System, error) {
|
|
if app.settings != nil && app.settings.Backups.S3.Enabled {
|
|
return filesystem.NewS3(
|
|
app.settings.Backups.S3.Bucket,
|
|
app.settings.Backups.S3.Region,
|
|
app.settings.Backups.S3.Endpoint,
|
|
app.settings.Backups.S3.AccessKey,
|
|
app.settings.Backups.S3.Secret,
|
|
app.settings.Backups.S3.ForcePathStyle,
|
|
)
|
|
}
|
|
|
|
// fallback to local filesystem
|
|
return filesystem.NewLocal(filepath.Join(app.DataDir(), LocalBackupsDirName))
|
|
}
|
|
|
|
// Restart restarts (aka. replaces) the current running application process.
|
|
//
|
|
// NB! It relies on execve which is supported only on UNIX based systems.
|
|
func (app *BaseApp) Restart() error {
|
|
if runtime.GOOS == "windows" {
|
|
return errors.New("restart is not supported on windows")
|
|
}
|
|
|
|
execPath, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
event := &TerminateEvent{}
|
|
event.App = app
|
|
event.IsRestart = true
|
|
|
|
return app.OnTerminate().Trigger(event, func(e *TerminateEvent) error {
|
|
_ = e.App.ResetBootstrapState()
|
|
|
|
// attempt to restart the bootstrap process in case execve returns an error for some reason
|
|
defer func() {
|
|
if err := e.App.Bootstrap(); err != nil {
|
|
app.Logger().Error("Failed to rebootstrap the application after failed app.Restart()", "error", err)
|
|
}
|
|
}()
|
|
|
|
return syscall.Exec(execPath, os.Args, os.Environ())
|
|
})
|
|
}
|
|
|
|
// RunSystemMigrations applies all new migrations registered in the [core.SystemMigrations] list.
|
|
func (app *BaseApp) RunSystemMigrations() error {
|
|
_, err := NewMigrationsRunner(app, SystemMigrations).Up()
|
|
return err
|
|
}
|
|
|
|
// RunAppMigrations applies all new migrations registered in the [core.AppMigrations] list.
|
|
func (app *BaseApp) RunAppMigrations() error {
|
|
_, err := NewMigrationsRunner(app, AppMigrations).Up()
|
|
return err
|
|
}
|
|
|
|
// RunAllMigrations applies all system and app migrations
|
|
// (aka. from both [core.SystemMigrations] and [core.AppMigrations]).
|
|
func (app *BaseApp) RunAllMigrations() error {
|
|
list := MigrationsList{}
|
|
list.Copy(SystemMigrations)
|
|
list.Copy(AppMigrations)
|
|
_, err := NewMigrationsRunner(app, list).Up()
|
|
return err
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// App event hooks
|
|
// -------------------------------------------------------------------
|
|
|
|
func (app *BaseApp) OnBootstrap() *hook.Hook[*BootstrapEvent] {
|
|
return app.onBootstrap
|
|
}
|
|
|
|
func (app *BaseApp) OnServe() *hook.Hook[*ServeEvent] {
|
|
return app.onServe
|
|
}
|
|
|
|
func (app *BaseApp) OnTerminate() *hook.Hook[*TerminateEvent] {
|
|
return app.onTerminate
|
|
}
|
|
|
|
func (app *BaseApp) OnBackupCreate() *hook.Hook[*BackupEvent] {
|
|
return app.onBackupCreate
|
|
}
|
|
|
|
func (app *BaseApp) OnBackupRestore() *hook.Hook[*BackupEvent] {
|
|
return app.onBackupRestore
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
func (app *BaseApp) OnModelCreate(tags ...string) *hook.TaggedHook[*ModelEvent] {
|
|
return hook.NewTaggedHook(app.onModelCreate, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnModelCreateExecute(tags ...string) *hook.TaggedHook[*ModelEvent] {
|
|
return hook.NewTaggedHook(app.onModelCreateExecute, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnModelAfterCreateSuccess(tags ...string) *hook.TaggedHook[*ModelEvent] {
|
|
return hook.NewTaggedHook(app.onModelAfterCreateSuccess, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnModelAfterCreateError(tags ...string) *hook.TaggedHook[*ModelErrorEvent] {
|
|
return hook.NewTaggedHook(app.onModelAfterCreateError, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnModelUpdate(tags ...string) *hook.TaggedHook[*ModelEvent] {
|
|
return hook.NewTaggedHook(app.onModelUpdate, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnModelUpdateExecute(tags ...string) *hook.TaggedHook[*ModelEvent] {
|
|
return hook.NewTaggedHook(app.onModelUpdateWrite, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnModelAfterUpdateSuccess(tags ...string) *hook.TaggedHook[*ModelEvent] {
|
|
return hook.NewTaggedHook(app.onModelAfterUpdateSuccess, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnModelAfterUpdateError(tags ...string) *hook.TaggedHook[*ModelErrorEvent] {
|
|
return hook.NewTaggedHook(app.onModelAfterUpdateError, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnModelValidate(tags ...string) *hook.TaggedHook[*ModelEvent] {
|
|
return hook.NewTaggedHook(app.onModelValidate, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnModelDelete(tags ...string) *hook.TaggedHook[*ModelEvent] {
|
|
return hook.NewTaggedHook(app.onModelDelete, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnModelDeleteExecute(tags ...string) *hook.TaggedHook[*ModelEvent] {
|
|
return hook.NewTaggedHook(app.onModelDeleteExecute, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnModelAfterDeleteSuccess(tags ...string) *hook.TaggedHook[*ModelEvent] {
|
|
return hook.NewTaggedHook(app.onModelAfterDeleteSuccess, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnModelAfterDeleteError(tags ...string) *hook.TaggedHook[*ModelErrorEvent] {
|
|
return hook.NewTaggedHook(app.onModelAfterDeleteError, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordEnrich(tags ...string) *hook.TaggedHook[*RecordEnrichEvent] {
|
|
return hook.NewTaggedHook(app.onRecordEnrich, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordValidate(tags ...string) *hook.TaggedHook[*RecordEvent] {
|
|
return hook.NewTaggedHook(app.onRecordValidate, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordCreate(tags ...string) *hook.TaggedHook[*RecordEvent] {
|
|
return hook.NewTaggedHook(app.onRecordCreate, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordCreateExecute(tags ...string) *hook.TaggedHook[*RecordEvent] {
|
|
return hook.NewTaggedHook(app.onRecordCreateExecute, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordAfterCreateSuccess(tags ...string) *hook.TaggedHook[*RecordEvent] {
|
|
return hook.NewTaggedHook(app.onRecordAfterCreateSuccess, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordAfterCreateError(tags ...string) *hook.TaggedHook[*RecordErrorEvent] {
|
|
return hook.NewTaggedHook(app.onRecordAfterCreateError, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordUpdate(tags ...string) *hook.TaggedHook[*RecordEvent] {
|
|
return hook.NewTaggedHook(app.onRecordUpdate, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordUpdateExecute(tags ...string) *hook.TaggedHook[*RecordEvent] {
|
|
return hook.NewTaggedHook(app.onRecordUpdateExecute, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordAfterUpdateSuccess(tags ...string) *hook.TaggedHook[*RecordEvent] {
|
|
return hook.NewTaggedHook(app.onRecordAfterUpdateSuccess, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordAfterUpdateError(tags ...string) *hook.TaggedHook[*RecordErrorEvent] {
|
|
return hook.NewTaggedHook(app.onRecordAfterUpdateError, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordDelete(tags ...string) *hook.TaggedHook[*RecordEvent] {
|
|
return hook.NewTaggedHook(app.onRecordDelete, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordDeleteExecute(tags ...string) *hook.TaggedHook[*RecordEvent] {
|
|
return hook.NewTaggedHook(app.onRecordDeleteExecute, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordAfterDeleteSuccess(tags ...string) *hook.TaggedHook[*RecordEvent] {
|
|
return hook.NewTaggedHook(app.onRecordAfterDeleteSuccess, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordAfterDeleteError(tags ...string) *hook.TaggedHook[*RecordErrorEvent] {
|
|
return hook.NewTaggedHook(app.onRecordAfterDeleteError, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionValidate(tags ...string) *hook.TaggedHook[*CollectionEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionValidate, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionCreate(tags ...string) *hook.TaggedHook[*CollectionEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionCreate, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionCreateExecute(tags ...string) *hook.TaggedHook[*CollectionEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionCreateExecute, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionAfterCreateSuccess(tags ...string) *hook.TaggedHook[*CollectionEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionAfterCreateSuccess, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionAfterCreateError(tags ...string) *hook.TaggedHook[*CollectionErrorEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionAfterCreateError, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionUpdate(tags ...string) *hook.TaggedHook[*CollectionEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionUpdate, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionUpdateExecute(tags ...string) *hook.TaggedHook[*CollectionEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionUpdateExecute, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionAfterUpdateSuccess(tags ...string) *hook.TaggedHook[*CollectionEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionAfterUpdateSuccess, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionAfterUpdateError(tags ...string) *hook.TaggedHook[*CollectionErrorEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionAfterUpdateError, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionDelete(tags ...string) *hook.TaggedHook[*CollectionEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionDelete, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionDeleteExecute(tags ...string) *hook.TaggedHook[*CollectionEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionDeleteExecute, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionAfterDeleteSuccess(tags ...string) *hook.TaggedHook[*CollectionEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionAfterDeleteSuccess, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionAfterDeleteError(tags ...string) *hook.TaggedHook[*CollectionErrorEvent] {
|
|
return hook.NewTaggedHook(app.onCollectionAfterDeleteError, tags...)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Mailer event hooks
|
|
// -------------------------------------------------------------------
|
|
|
|
func (app *BaseApp) OnMailerSend() *hook.Hook[*MailerEvent] {
|
|
return app.onMailerSend
|
|
}
|
|
|
|
func (app *BaseApp) OnMailerRecordPasswordResetSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] {
|
|
return hook.NewTaggedHook(app.onMailerRecordPasswordResetSend, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnMailerRecordVerificationSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] {
|
|
return hook.NewTaggedHook(app.onMailerRecordVerificationSend, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnMailerRecordEmailChangeSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] {
|
|
return hook.NewTaggedHook(app.onMailerRecordEmailChangeSend, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnMailerRecordOTPSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] {
|
|
return hook.NewTaggedHook(app.onMailerRecordOTPSend, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnMailerRecordAuthAlertSend(tags ...string) *hook.TaggedHook[*MailerRecordEvent] {
|
|
return hook.NewTaggedHook(app.onMailerRecordAuthAlertSend, tags...)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Realtime API event hooks
|
|
// -------------------------------------------------------------------
|
|
|
|
func (app *BaseApp) OnRealtimeConnectRequest() *hook.Hook[*RealtimeConnectRequestEvent] {
|
|
return app.onRealtimeConnectRequest
|
|
}
|
|
|
|
func (app *BaseApp) OnRealtimeMessageSend() *hook.Hook[*RealtimeMessageEvent] {
|
|
return app.onRealtimeMessageSend
|
|
}
|
|
|
|
func (app *BaseApp) OnRealtimeSubscribeRequest() *hook.Hook[*RealtimeSubscribeRequestEvent] {
|
|
return app.onRealtimeSubscribeRequest
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Settings API event hooks
|
|
// -------------------------------------------------------------------
|
|
|
|
func (app *BaseApp) OnSettingsListRequest() *hook.Hook[*SettingsListRequestEvent] {
|
|
return app.onSettingsListRequest
|
|
}
|
|
|
|
func (app *BaseApp) OnSettingsUpdateRequest() *hook.Hook[*SettingsUpdateRequestEvent] {
|
|
return app.onSettingsUpdateRequest
|
|
}
|
|
|
|
func (app *BaseApp) OnSettingsReload() *hook.Hook[*SettingsReloadEvent] {
|
|
return app.onSettingsReload
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// File API event hooks
|
|
// -------------------------------------------------------------------
|
|
|
|
func (app *BaseApp) OnFileDownloadRequest(tags ...string) *hook.TaggedHook[*FileDownloadRequestEvent] {
|
|
return hook.NewTaggedHook(app.onFileDownloadRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnFileTokenRequest(tags ...string) *hook.TaggedHook[*FileTokenRequestEvent] {
|
|
return hook.NewTaggedHook(app.onFileTokenRequest, tags...)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Record auth API event hooks
|
|
// -------------------------------------------------------------------
|
|
|
|
func (app *BaseApp) OnRecordAuthRequest(tags ...string) *hook.TaggedHook[*RecordAuthRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordAuthRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordAuthWithPasswordRequest(tags ...string) *hook.TaggedHook[*RecordAuthWithPasswordRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordAuthWithPasswordRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordAuthWithOAuth2Request(tags ...string) *hook.TaggedHook[*RecordAuthWithOAuth2RequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordAuthWithOAuth2Request, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordAuthRefreshRequest(tags ...string) *hook.TaggedHook[*RecordAuthRefreshRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordAuthRefreshRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordRequestPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordRequestPasswordResetRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordRequestPasswordResetRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordConfirmPasswordResetRequest(tags ...string) *hook.TaggedHook[*RecordConfirmPasswordResetRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordConfirmPasswordResetRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordRequestVerificationRequest(tags ...string) *hook.TaggedHook[*RecordRequestVerificationRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordRequestVerificationRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordConfirmVerificationRequest(tags ...string) *hook.TaggedHook[*RecordConfirmVerificationRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordConfirmVerificationRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordRequestEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordRequestEmailChangeRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordRequestEmailChangeRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordConfirmEmailChangeRequest(tags ...string) *hook.TaggedHook[*RecordConfirmEmailChangeRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordConfirmEmailChangeRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordRequestOTPRequest(tags ...string) *hook.TaggedHook[*RecordCreateOTPRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordRequestOTPRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordAuthWithOTPRequest(tags ...string) *hook.TaggedHook[*RecordAuthWithOTPRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordAuthWithOTPRequest, tags...)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Record CRUD API event hooks
|
|
// -------------------------------------------------------------------
|
|
|
|
func (app *BaseApp) OnRecordsListRequest(tags ...string) *hook.TaggedHook[*RecordsListRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordsListRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordViewRequest(tags ...string) *hook.TaggedHook[*RecordRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordViewRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordCreateRequest(tags ...string) *hook.TaggedHook[*RecordRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordCreateRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordUpdateRequest(tags ...string) *hook.TaggedHook[*RecordRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordUpdateRequest, tags...)
|
|
}
|
|
|
|
func (app *BaseApp) OnRecordDeleteRequest(tags ...string) *hook.TaggedHook[*RecordRequestEvent] {
|
|
return hook.NewTaggedHook(app.onRecordDeleteRequest, tags...)
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Collection API event hooks
|
|
// -------------------------------------------------------------------
|
|
|
|
func (app *BaseApp) OnCollectionsListRequest() *hook.Hook[*CollectionsListRequestEvent] {
|
|
return app.onCollectionsListRequest
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionViewRequest() *hook.Hook[*CollectionRequestEvent] {
|
|
return app.onCollectionViewRequest
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionCreateRequest() *hook.Hook[*CollectionRequestEvent] {
|
|
return app.onCollectionCreateRequest
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionUpdateRequest() *hook.Hook[*CollectionRequestEvent] {
|
|
return app.onCollectionUpdateRequest
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionDeleteRequest() *hook.Hook[*CollectionRequestEvent] {
|
|
return app.onCollectionDeleteRequest
|
|
}
|
|
|
|
func (app *BaseApp) OnCollectionsImportRequest() *hook.Hook[*CollectionsImportRequestEvent] {
|
|
return app.onCollectionsImportRequest
|
|
}
|
|
|
|
func (app *BaseApp) OnBatchRequest() *hook.Hook[*BatchRequestEvent] {
|
|
return app.onBatchRequest
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Helpers
|
|
// -------------------------------------------------------------------
|
|
|
|
func (app *BaseApp) initDataDB() error {
|
|
dbPath := filepath.Join(app.DataDir(), "data.db")
|
|
|
|
concurrentDB, err := app.config.DBConnect(dbPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
concurrentDB.DB().SetMaxOpenConns(app.config.DataMaxOpenConns)
|
|
concurrentDB.DB().SetMaxIdleConns(app.config.DataMaxIdleConns)
|
|
concurrentDB.DB().SetConnMaxIdleTime(3 * time.Minute)
|
|
|
|
nonconcurrentDB, err := app.config.DBConnect(dbPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nonconcurrentDB.DB().SetMaxOpenConns(1)
|
|
nonconcurrentDB.DB().SetMaxIdleConns(1)
|
|
nonconcurrentDB.DB().SetConnMaxIdleTime(3 * time.Minute)
|
|
|
|
if app.IsDev() {
|
|
nonconcurrentDB.QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
|
color.HiBlack("[%.2fms] %v\n", float64(t.Milliseconds()), normalizeSQLLog(sql))
|
|
}
|
|
nonconcurrentDB.ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
|
color.HiBlack("[%.2fms] %v\n", float64(t.Milliseconds()), normalizeSQLLog(sql))
|
|
}
|
|
concurrentDB.QueryLogFunc = nonconcurrentDB.QueryLogFunc
|
|
concurrentDB.ExecLogFunc = nonconcurrentDB.ExecLogFunc
|
|
}
|
|
|
|
app.concurrentDB = concurrentDB
|
|
app.nonconcurrentDB = nonconcurrentDB
|
|
|
|
return nil
|
|
}
|
|
|
|
var sqlLogReplacements = map[string]string{
|
|
"{{": "`",
|
|
"}}": "`",
|
|
"[[": "`",
|
|
"]]": "`",
|
|
"<nil>": "NULL",
|
|
}
|
|
var sqlLogPrefixedTableIdentifierPattern = regexp.MustCompile(`\[\[(.+)\.(.+)\]\]`)
|
|
var sqlLogPrefixedColumnIdentifierPattern = regexp.MustCompile(`\{\{(.+)\.(.+)\}\}`)
|
|
|
|
// normalizeSQLLog replaces common query builder charactes with their plain SQL version for easier debugging.
|
|
// The query is still not suitable for execution and should be used only for log and debug purposes
|
|
// (the normalization is done here to avoid breaking changes in dbx).
|
|
func normalizeSQLLog(sql string) string {
|
|
sql = sqlLogPrefixedTableIdentifierPattern.ReplaceAllString(sql, "`$1`.`$2`")
|
|
|
|
sql = sqlLogPrefixedColumnIdentifierPattern.ReplaceAllString(sql, "`$1`.`$2`")
|
|
|
|
for old, new := range sqlLogReplacements {
|
|
sql = strings.ReplaceAll(sql, old, new)
|
|
}
|
|
|
|
return sql
|
|
}
|
|
|
|
func (app *BaseApp) initAuxDB() error {
|
|
// note: renamed to "auxiliary" because "aux" is a reserved Windows filename
|
|
// (see https://github.com/pocketbase/pocketbase/issues/5607)
|
|
dbPath := filepath.Join(app.DataDir(), "auxiliary.db")
|
|
|
|
concurrentDB, err := app.config.DBConnect(dbPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
concurrentDB.DB().SetMaxOpenConns(app.config.AuxMaxOpenConns)
|
|
concurrentDB.DB().SetMaxIdleConns(app.config.AuxMaxIdleConns)
|
|
concurrentDB.DB().SetConnMaxIdleTime(3 * time.Minute)
|
|
|
|
nonconcurrentDB, err := app.config.DBConnect(dbPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nonconcurrentDB.DB().SetMaxOpenConns(1)
|
|
nonconcurrentDB.DB().SetMaxIdleConns(1)
|
|
nonconcurrentDB.DB().SetConnMaxIdleTime(3 * time.Minute)
|
|
|
|
app.auxConcurrentDB = concurrentDB
|
|
app.auxNonconcurrentDB = nonconcurrentDB
|
|
|
|
return nil
|
|
}
|
|
|
|
func (app *BaseApp) registerBaseHooks() {
|
|
deletePrefix := func(prefix string) error {
|
|
fs, err := app.NewFilesystem()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fs.Close()
|
|
|
|
failed := fs.DeletePrefix(prefix)
|
|
if len(failed) > 0 {
|
|
return errors.New("failed to delete the files at " + prefix)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// try to delete the storage files from deleted Collection, Records, etc. model
|
|
app.OnModelAfterDeleteSuccess().Bind(&hook.Handler[*ModelEvent]{
|
|
Id: "__pbFilesManagerDelete__",
|
|
Func: func(e *ModelEvent) error {
|
|
if m, ok := e.Model.(FilesManager); ok && m.BaseFilesPath() != "" {
|
|
// ensure that there is a trailing slash so that the list iterator could start walking from the prefix dir
|
|
// (https://github.com/pocketbase/pocketbase/discussions/5246#discussioncomment-10128955)
|
|
prefix := strings.TrimRight(m.BaseFilesPath(), "/") + "/"
|
|
|
|
// run in the background for "optimistic" delete to avoid
|
|
// blocking the delete transaction
|
|
routine.FireAndForget(func() {
|
|
if err := deletePrefix(prefix); err != nil {
|
|
app.Logger().Error(
|
|
"Failed to delete storage prefix (non critical error; usually could happen because of S3 api limits)",
|
|
slog.String("prefix", prefix),
|
|
slog.String("error", err.Error()),
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
return e.Next()
|
|
},
|
|
Priority: -99,
|
|
})
|
|
|
|
app.OnServe().Bind(&hook.Handler[*ServeEvent]{
|
|
Id: "__pbCronStart__",
|
|
Func: func(e *ServeEvent) error {
|
|
app.Cron().Start()
|
|
|
|
return e.Next()
|
|
},
|
|
Priority: 999,
|
|
})
|
|
|
|
app.Cron().Add("__pbPragmaOptimize__", "0 0 * * *", func() {
|
|
_, execErr := app.DB().NewQuery("PRAGMA optimize").Execute()
|
|
if execErr != nil {
|
|
app.Logger().Warn("Failed to run periodic PRAGMA optimize", slog.String("error", execErr.Error()))
|
|
}
|
|
})
|
|
|
|
app.registerSettingsHooks()
|
|
app.registerAutobackupHooks()
|
|
app.registerCollectionHooks()
|
|
app.registerRecordHooks()
|
|
app.registerSuperuserHooks()
|
|
app.registerExternalAuthHooks()
|
|
app.registerMFAHooks()
|
|
app.registerOTPHooks()
|
|
app.registerAuthOriginHooks()
|
|
}
|
|
|
|
// getLoggerMinLevel returns the logger min level based on the
|
|
// app configurations (dev mode, settings, etc.).
|
|
//
|
|
// If not in dev mode - returns the level from the app settings.
|
|
//
|
|
// If the app is in dev mode it returns -9999 level allowing to print
|
|
// practically all logs to the terminal.
|
|
// In this case DB logs are still filtered but the checks for the min level are done
|
|
// in the BatchOptions.BeforeAddFunc instead of the slog.Handler.Enabled() method.
|
|
func getLoggerMinLevel(app App) slog.Level {
|
|
var minLevel slog.Level
|
|
|
|
if app.IsDev() {
|
|
minLevel = -99999
|
|
} else if app.Settings() != nil {
|
|
minLevel = slog.Level(app.Settings().Logs.MinLevel)
|
|
}
|
|
|
|
return minLevel
|
|
}
|
|
|
|
func (app *BaseApp) initLogger() error {
|
|
duration := 3 * time.Second
|
|
ticker := time.NewTicker(duration)
|
|
done := make(chan bool)
|
|
|
|
handler := logger.NewBatchHandler(logger.BatchOptions{
|
|
Level: getLoggerMinLevel(app),
|
|
BatchSize: 200,
|
|
BeforeAddFunc: func(ctx context.Context, log *logger.Log) bool {
|
|
if app.IsDev() {
|
|
printLog(log)
|
|
|
|
// manually check the log level and skip if necessary
|
|
if log.Level < slog.Level(app.Settings().Logs.MinLevel) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
ticker.Reset(duration)
|
|
|
|
return app.Settings().Logs.MaxDays > 0
|
|
},
|
|
WriteFunc: func(ctx context.Context, logs []*logger.Log) error {
|
|
if !app.IsBootstrapped() || app.Settings().Logs.MaxDays == 0 {
|
|
return nil
|
|
}
|
|
|
|
// write the accumulated logs
|
|
// (note: based on several local tests there is no significant performance difference between small number of separate write queries vs 1 big INSERT)
|
|
app.AuxRunInTransaction(func(txApp App) error {
|
|
model := &Log{}
|
|
for _, l := range logs {
|
|
model.MarkAsNew()
|
|
model.Id = GenerateDefaultRandomId()
|
|
model.Level = int(l.Level)
|
|
model.Message = l.Message
|
|
model.Data = l.Data
|
|
model.Created, _ = types.ParseDateTime(l.Time)
|
|
|
|
if err := txApp.AuxSave(model); err != nil {
|
|
log.Println("Failed to write log", model, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return nil
|
|
},
|
|
})
|
|
|
|
go func() {
|
|
ctx := context.Background()
|
|
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
case <-ticker.C:
|
|
handler.WriteAll(ctx)
|
|
}
|
|
}
|
|
}()
|
|
|
|
app.logger = slog.New(handler)
|
|
|
|
// write all remaining logs before ticker.Stop to avoid races with ResetBootstrap user calls
|
|
app.OnTerminate().Bind(&hook.Handler[*TerminateEvent]{
|
|
Id: "__pbAppLoggerOnTerminate__",
|
|
Func: func(e *TerminateEvent) error {
|
|
handler.WriteAll(context.Background())
|
|
|
|
ticker.Stop()
|
|
|
|
done <- true
|
|
|
|
return e.Next()
|
|
},
|
|
Priority: -999,
|
|
})
|
|
|
|
// reload log handler level (if initialized)
|
|
app.OnSettingsReload().Bind(&hook.Handler[*SettingsReloadEvent]{
|
|
Id: "__pbAppLoggerOnSettingsReload__",
|
|
Func: func(e *SettingsReloadEvent) error {
|
|
err := e.Next()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if e.App.Logger() != nil {
|
|
if h, ok := e.App.Logger().Handler().(*logger.BatchHandler); ok {
|
|
h.SetLevel(getLoggerMinLevel(e.App))
|
|
}
|
|
}
|
|
|
|
// try to clear old logs not matching the new settings
|
|
createdBefore := types.NowDateTime().AddDate(0, 0, -1*e.App.Settings().Logs.MaxDays)
|
|
expr := dbx.NewExp("[[created]] <= {:date} OR [[level]] < {:level}", dbx.Params{
|
|
"date": createdBefore.String(),
|
|
"level": e.App.Settings().Logs.MinLevel,
|
|
})
|
|
_, err = e.App.AuxNonconcurrentDB().Delete((&Log{}).TableName(), expr).Execute()
|
|
if err != nil {
|
|
e.App.Logger().Debug("Failed to cleanup old logs", "error", err)
|
|
}
|
|
|
|
// no logs are allowed -> try to reclaim preserved disk space after the previous delete operation
|
|
if e.App.Settings().Logs.MaxDays == 0 {
|
|
err = e.App.AuxVacuum()
|
|
if err != nil {
|
|
e.App.Logger().Debug("Failed to VACUUM aux database", "error", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Priority: -999,
|
|
})
|
|
|
|
// cleanup old logs
|
|
app.Cron().Add("__pbLogsCleanup__", "0 */6 * * *", func() {
|
|
deleteErr := app.DeleteOldLogs(time.Now().AddDate(0, 0, -1*app.Settings().Logs.MaxDays))
|
|
if deleteErr != nil {
|
|
app.Logger().Warn("Failed to delete old logs", "error", deleteErr)
|
|
}
|
|
})
|
|
|
|
return nil
|
|
}
|