package services import ( "context" "database/sql" "fmt" "github.com/mikestefanello/backlite" "log/slog" "os" "strings" entsql "entgo.io/ent/dialect/sql" "github.com/labstack/echo/v4" _ "github.com/mattn/go-sqlite3" "github.com/mikestefanello/pagoda/config" "github.com/mikestefanello/pagoda/ent" "github.com/mikestefanello/pagoda/pkg/funcmap" "github.com/mikestefanello/pagoda/pkg/log" // Require by ent _ "github.com/mikestefanello/pagoda/ent/runtime" ) // Container contains all services used by the application and provides an easy way to handle dependency // injection including within tests type Container struct { // Validator stores a validator Validator *Validator // Web stores the web framework Web *echo.Echo // Config stores the application configuration Config *config.Config // Cache contains the cache client Cache *CacheClient // Database stores the connection to the database Database *sql.DB // ORM stores a client to the ORM ORM *ent.Client // Mail stores an email sending client Mail *MailClient // Auth stores an authentication client Auth *AuthClient // TemplateRenderer stores a service to easily render and cache templates TemplateRenderer *TemplateRenderer // Tasks stores the task client Tasks *backlite.Client } // NewContainer creates and initializes a new Container func NewContainer() *Container { c := new(Container) c.initConfig() c.initValidator() c.initWeb() c.initCache() c.initDatabase() c.initORM() c.initAuth() c.initTemplateRenderer() c.initMail() c.initTasks() return c } // Shutdown gracefully shuts the Container down and disconnects all connections. func (c *Container) Shutdown() error { // Shutdown the web server. webCtx, webCancel := context.WithTimeout(context.Background(), c.Config.HTTP.ShutdownTimeout) defer webCancel() if err := c.Web.Shutdown(webCtx); err != nil { return err } // Shutdown the task runner. taskCtx, taskCancel := context.WithTimeout(context.Background(), c.Config.Tasks.ShutdownTimeout) defer taskCancel() c.Tasks.Stop(taskCtx) // Shutdown the ORM. if err := c.ORM.Close(); err != nil { return err } // Shutdown the database. if err := c.Database.Close(); err != nil { return err } // Shutdown the cache. c.Cache.Close() return nil } // initConfig initializes configuration func (c *Container) initConfig() { cfg, err := config.GetConfig() if err != nil { panic(fmt.Sprintf("failed to load config: %v", err)) } c.Config = &cfg // Configure logging switch cfg.App.Environment { case config.EnvProduction: slog.SetLogLoggerLevel(slog.LevelInfo) default: slog.SetLogLoggerLevel(slog.LevelDebug) } } // initValidator initializes the validator func (c *Container) initValidator() { c.Validator = NewValidator() } // initWeb initializes the web framework func (c *Container) initWeb() { c.Web = echo.New() c.Web.HideBanner = true c.Web.Validator = c.Validator } // initCache initializes the cache func (c *Container) initCache() { store, err := newInMemoryCache(c.Config.Cache.Capacity) if err != nil { panic(err) } c.Cache = NewCacheClient(store) } // initDatabase initializes the database func (c *Container) initDatabase() { var err error var connection string switch c.Config.App.Environment { case config.EnvTest: // TODO: Drop/recreate the DB, if this isn't in memory? connection = c.Config.Database.TestConnection default: connection = c.Config.Database.Connection } c.Database, err = openDB(c.Config.Database.Driver, connection) if err != nil { panic(err) } } // initORM initializes the ORM func (c *Container) initORM() { drv := entsql.OpenDB(c.Config.Database.Driver, c.Database) c.ORM = ent.NewClient(ent.Driver(drv)) // Run the auto migration tool. if err := c.ORM.Schema.Create(context.Background()); err != nil { panic(err) } } // initAuth initializes the authentication client func (c *Container) initAuth() { c.Auth = NewAuthClient(c.Config, c.ORM) } // initTemplateRenderer initializes the template renderer func (c *Container) initTemplateRenderer() { c.TemplateRenderer = NewTemplateRenderer(c.Config, c.Cache, funcmap.NewFuncMap(c.Web)) } // initMail initialize the mail client func (c *Container) initMail() { var err error c.Mail, err = NewMailClient(c.Config, c.TemplateRenderer) if err != nil { panic(fmt.Sprintf("failed to create mail client: %v", err)) } } // initTasks initializes the task client func (c *Container) initTasks() { var err error // You could use a separate database for tasks, if you'd like. but using one // makes transaction support easier c.Tasks, err = backlite.NewClient(backlite.ClientConfig{ DB: c.Database, Logger: log.Default(), NumWorkers: c.Config.Tasks.Goroutines, ReleaseAfter: c.Config.Tasks.ReleaseAfter, CleanupInterval: c.Config.Tasks.CleanupInterval, }) if err != nil { panic(fmt.Sprintf("failed to create task client: %v", err)) } if err = c.Tasks.Install(); err != nil { panic(fmt.Sprintf("failed to install task schema: %v", err)) } } // openDB opens a database connection func openDB(driver, connection string) (*sql.DB, error) { // Helper to automatically create the directories that the specified sqlite file // should reside in, if one if driver == "sqlite3" { d := strings.Split(connection, "/") if len(d) > 1 { path := strings.Join(d[:len(d)-1], "/") if err := os.MkdirAll(path, 0755); err != nil { return nil, err } } } return sql.Open(driver, connection) }