package servicecontext import ( "context" "errors" "example.com/userService/internal/middleware" userrepo "example.com/userService/internal/repository/user" "example.com/userService/internal/settings" "git.yuntuo.hk/k2safe/go-omni/database" "git.yuntuo.hk/k2safe/go-omni/health" "git.yuntuo.hk/k2safe/go-omni/httpx" "git.yuntuo.hk/k2safe/go-omni/logx" "git.yuntuo.hk/k2safe/go-omni/storage" "github.com/go-sql-driver/mysql" "net/http" "sync" "time" ) // Context owns infrastructure, while Logger is borrowed from main. // Add business repositories here; HTTP contracts do not define database tables. type Context struct { Users userrepo.UnitOfWork Logger *logx.Logger Health *health.Checker Middlewares map[string]httpx.Middleware DB *database.DB Files storage.Store Uploads *storage.Uploader Downloads storage.Presigner UploadLinks storage.UploadPresigner FileHTTP http.Handler storageClose func() error closeMu sync.Mutex closed bool closeOnce sync.Once closeErr error } // Pass the settings loaded by main. The optional form supports lightweight // tests without infrastructure; selected adapters still require valid settings. func New(ctx context.Context, logger *logx.Logger, configs ...settings.Config) (result *Context, err error) { if ctx == nil { ctx = context.Background() } if err = ctx.Err(); err != nil { return nil, err } if len(configs) > 1 { return nil, errors.New("expected at most one configuration") } cfg := settings.Default() if len(configs) == 1 { cfg = configs[0] } _ = cfg checker, err := health.New() if err != nil { return nil, err } c := &Context{Logger: logger, Health: checker, Middlewares: middleware.Defaults()} defer func() { if err != nil { err = errors.Join(err, c.Close()) } }() if err = cfg.Validate(); err != nil { return nil, err } dsn, parseErr := mysql.ParseDSN(cfg.Database.DSN) if parseErr != nil || dsn.DBName == "" { return nil, errors.New("GO_OMNI_DB_DSN must contain a valid MySQL database name") } dsn.ParseTime = true dsn.Loc = time.UTC cfg.Database.DSN = dsn.FormatDSN() c.DB, err = database.Open(ctx, cfg.Database) if err != nil { return nil, err } if err = checker.Add(health.Probe{Name: "database", Timeout: cfg.Database.PingTimeout, Check: c.DB.Health}); err != nil { return nil, err } if err = c.InitStorage(ctx, cfg.Storage); err != nil { return nil, err } c.Users = userrepo.NewStore(c.DB) checkCtx, cancel := context.WithTimeout(ctx, cfg.Database.PingTimeout) defer cancel() if err = userrepo.NewStore(c.DB).CheckSchema(checkCtx); err != nil { return nil, errors.New("user schema is missing or incompatible; apply migrations first") } authHook, authErr := middleware.NewAuthFromEnv() if authErr != nil { return nil, authErr } c.Middlewares["Auth"] = authHook return c, nil } // Close is idempotent, including startup failure and graceful shutdown. func (c *Context) Close() error { if c == nil { return nil } c.closeMu.Lock() defer c.closeMu.Unlock() c.closed = true c.closeOnce.Do(func() { if c.storageClose != nil { c.closeErr = errors.Join(c.closeErr, c.storageClose()) } if c.DB != nil { c.closeErr = errors.Join(c.closeErr, c.DB.Close()) } }) return c.closeErr }