// Package classes holds the SEO plugin's logic: the service every writer // goes through, the entry store and the effective-SEO resolver. package classes import ( "context" "errors" "sync/atomic" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/cabana" "git.golem15.com/golem15/summercms/modules/compass" "git.golem15.com/golem15/summercms/modules/signpost" "gorm.io/gorm" ) // ErrNotBooted is returned when the service has no booted application. var ErrNotBooted = errors.New("golem15.seo: app is not booted") // Service is the plugin's single entry point for reads and writes of SEO // data. The admin extension, the admin controllers, the listeners, the job // and the API all call it, so every write goes through the same checks and // history. type Service struct { app *backpack.App // robots is the last production robots.txt served, answered when the // store fails (D-22: never a 5xx). robots atomic.Pointer[string] } // NewService returns the service of a booted application. The database // and the signpost registry are looked up when used, so a binary that runs // a console command without a database still boots. func NewService(app *backpack.App) (*Service, error) { if app == nil { return nil, ErrNotBooted } return &Service{app: app}, nil } // App returns the booted application. func (s *Service) App() *backpack.App { if s == nil { return nil } return s.app } type txKey struct{} // WithTx returns ctx carrying tx, the transaction an API write or a batch // runs in, so every read of the service (the resolver, the redirect set, // the settings) sees the uncommitted changes of that transaction. func WithTx(ctx context.Context, tx *gorm.DB) context.Context { if tx == nil { return ctx } return context.WithValue(ctx, txKey{}, tx) } // DB returns the transaction carried by ctx (WithTx, else the admin write // transaction of cabana.TxFromContext), else the application's database // bound to ctx. func (s *Service) DB(ctx context.Context) (*gorm.DB, error) { if tx, ok := ctx.Value(txKey{}).(*gorm.DB); ok && tx != nil { return tx, nil } if tx, ok := cabana.TxFromContext(ctx); ok && tx != nil { return tx, nil } if s == nil || s.app == nil { return nil, ErrNotBooted } db, ok := s.app.Lookup[*gorm.DB]() if !ok || db == nil { return nil, errors.New("golem15.seo: database is not configured") } return db.WithContext(ctx), nil } // Registry returns the menu item type registry, nil when none is published. func (s *Service) Registry() *signpost.Registry { if s == nil || s.app == nil { return nil } reg, _ := s.app.Lookup[*signpost.Registry]() return reg } // GormDB returns the application's database without a transaction, the // handle lagoon.Transaction opens a write on. func (s *Service) GormDB() (*gorm.DB, error) { if s == nil || s.app == nil { return nil, ErrNotBooted } db, ok := s.app.Lookup[*gorm.DB]() if !ok || db == nil { return nil, errors.New("golem15.seo: database is not configured") } return db, nil } // Config returns the application config, nil when there is none. func (s *Service) Config() *compass.Config { if s == nil || s.app == nil { return nil } return s.app.Config }