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 }