feat(11-01): add lagoon.OnDatabase and after-commit transactions
- 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
This commit is contained in:
95
modules/lagoon/ondatabase.go
Normal file
95
modules/lagoon/ondatabase.go
Normal file
@@ -0,0 +1,95 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user