- OnDatabase runs a callback once the database is published (now, or when lagoon.Publish runs), so GORM callbacks registered at Boot also install under serve, where Boot runs before the database is opened - Transaction runs AfterCommit callbacks in order after a successful commit; nested calls are savepoints whose callbacks drop with them - the lagoon:after_commit GORM callback flushes single-statement AfterCommit work after GORM's own commit; outside a transaction it runs immediately
96 lines
2.5 KiB
Go
96 lines
2.5 KiB
Go
package lagoon
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// databaseHooks is the per-app queue of OnDatabase callbacks waiting for
|
|
// Publish. It lives on the app, never in a package variable.
|
|
type databaseHooks struct {
|
|
mu sync.Mutex
|
|
pending []func(*sql.DB, *gorm.DB) error
|
|
}
|
|
|
|
// OnDatabase runs fn with the shared pool and GORM handle as soon as the
|
|
// database is available: immediately when the app has already published a
|
|
// *gorm.DB, otherwise when Publish runs. Plugins register GORM callbacks
|
|
// through it from Boot, which runs before the serve command publishes the
|
|
// database. The error of an immediate call is returned; the error of a
|
|
// queued call is returned by Publish.
|
|
func OnDatabase(app *backpack.App, fn func(sqlDB *sql.DB, gdb *gorm.DB) error) error {
|
|
if app == nil {
|
|
return fmt.Errorf("lagoon: app is nil")
|
|
}
|
|
if fn == nil {
|
|
return fmt.Errorf("lagoon: database hook is nil")
|
|
}
|
|
h, err := hooksFor(app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h.mu.Lock()
|
|
sqlDB, gdb, ok := publishedDB(app)
|
|
if !ok {
|
|
h.pending = append(h.pending, fn)
|
|
h.mu.Unlock()
|
|
return nil
|
|
}
|
|
h.mu.Unlock()
|
|
return fn(sqlDB, gdb)
|
|
}
|
|
|
|
// runDatabaseHooks drains the OnDatabase queue once the handles are
|
|
// published and returns the first error.
|
|
func runDatabaseHooks(app *backpack.App, sqlDB *sql.DB, gdb *gorm.DB) error {
|
|
h, ok := app.Lookup[*databaseHooks]()
|
|
if !ok || h == nil {
|
|
return nil
|
|
}
|
|
h.mu.Lock()
|
|
pending := h.pending
|
|
h.pending = nil
|
|
h.mu.Unlock()
|
|
for _, fn := range pending {
|
|
if err := fn(sqlDB, gdb); err != nil {
|
|
return fmt.Errorf("lagoon: database hook: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hooksFor(app *backpack.App) (*databaseHooks, error) {
|
|
if h, ok := app.Lookup[*databaseHooks](); ok && h != nil {
|
|
return h, nil
|
|
}
|
|
h := &databaseHooks{}
|
|
if err := app.Publish(h); err != nil {
|
|
if existing, ok := app.Lookup[*databaseHooks](); ok && existing != nil {
|
|
return existing, nil
|
|
}
|
|
return nil, fmt.Errorf("lagoon: %w", err)
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
// publishedDB returns the published handles. A published *gorm.DB is
|
|
// enough: its own pool stands in when no *sql.DB was published.
|
|
func publishedDB(app *backpack.App) (*sql.DB, *gorm.DB, bool) {
|
|
gdb, ok := app.Lookup[*gorm.DB]()
|
|
if !ok || gdb == nil {
|
|
return nil, nil, false
|
|
}
|
|
if sqlDB, ok := app.Lookup[*sql.DB](); ok && sqlDB != nil {
|
|
return sqlDB, gdb, true
|
|
}
|
|
sqlDB, err := gdb.DB()
|
|
if err != nil {
|
|
return nil, nil, false
|
|
}
|
|
return sqlDB, gdb, true
|
|
}
|