package service import ( "context" "errors" "fmt" "os" "strings" "sync" "time" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) var ( backendStore Store = unavailableStore{err: errors.New("backend store not initialized")} defaultStoreOnce sync.Once defaultStoreErr error ) func setBackendStore(store Store) { if store == nil { backendStore = unavailableStore{err: errors.New("backend store not initialized")} return } backendStore = store } func bootstrapDefaultStore() (Store, error) { defaultStoreOnce.Do(func() { db, err := openDBFromEnv() if err != nil { defaultStoreErr = err setBackendStore(unavailableStore{err: err}) return } store, err := newGORMStore(db) if err != nil { defaultStoreErr = err setBackendStore(unavailableStore{err: err}) return } setBackendStore(store) }) return backendStore, defaultStoreErr } func openDBFromEnv() (*gorm.DB, error) { dsn := strings.TrimSpace(os.Getenv("DATABASE_URL")) if dsn == "" { return nil, errors.New("DATABASE_URL is required") } return openDB(dsn) } func openDB(dsn string) (*gorm.DB, error) { config := &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)} var ( db *gorm.DB err error ) if strings.HasPrefix(dsn, "sqlite:") || strings.HasPrefix(dsn, "file:") || dsn == ":memory:" { db, err = gorm.Open(sqlite.Open(dsn), config) } else { db, err = gorm.Open(postgres.Open(dsn), config) } if err != nil { return nil, fmt.Errorf("open database: %w", err) } sqlDB, err := db.DB() if err != nil { return nil, fmt.Errorf("database handle: %w", err) } sqlDB.SetConnMaxLifetime(5 * time.Minute) sqlDB.SetMaxIdleConns(5) sqlDB.SetMaxOpenConns(10) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := sqlDB.PingContext(ctx); err != nil { return nil, fmt.Errorf("ping database: %w", err) } return db, nil } type unavailableStore struct { err error } func (s unavailableStore) Ping(context.Context) error { return s.err } func (s unavailableStore) UserByLogin(context.Context, string) (UserProfile, bool, error) { return UserProfile{}, false, s.err } func (s unavailableStore) UserByID(context.Context, string) (UserProfile, bool, error) { return UserProfile{}, false, s.err } func (s unavailableStore) AddUser(context.Context, string, string, string) (UserProfile, error) { return UserProfile{}, s.err } func (s unavailableStore) UpdatePassword(context.Context, string, string) error { return s.err } func (s unavailableStore) ListUsers(context.Context) ([]UserProfile, error) { return nil, s.err } func (s unavailableStore) ListContent(context.Context, ContentFilter) ([]ContentItem, error) { return nil, s.err } func (s unavailableStore) ContentByID(context.Context, string) (ContentItem, bool, error) { return ContentItem{}, false, s.err } func (s unavailableStore) AddContent(context.Context, ContentItem) (ContentItem, error) { return ContentItem{}, s.err } func (s unavailableStore) PatchContent(context.Context, string, ContentItem) (ContentItem, bool, error) { return ContentItem{}, false, s.err } func (s unavailableStore) DeleteContent(context.Context, string) (bool, error) { return false, s.err } func (s unavailableStore) ListCategories(context.Context) ([]string, error) { return nil, s.err } func (s unavailableStore) ListTags(context.Context) ([]string, error) { return nil, s.err } func (s unavailableStore) ListSpeakers(context.Context) ([]Speaker, error) { return nil, s.err } func (s unavailableStore) AddFile(context.Context, StoredFile) (StoredFile, error) { return StoredFile{}, s.err } func (s unavailableStore) FileByID(context.Context, string) (StoredFile, bool, error) { return StoredFile{}, false, s.err } func (s unavailableStore) UpsertSubscription(context.Context, string, string) ([]string, error) { return nil, s.err } func (s unavailableStore) ListNotifications(context.Context) ([]NotificationItem, error) { return nil, s.err } func (s unavailableStore) MarkNotificationRead(context.Context, string) (*NotificationItem, error) { return nil, s.err } func (s unavailableStore) ListComments(context.Context, string) ([]CommentItem, error) { return nil, s.err } func (s unavailableStore) AddComment(context.Context, string, UserProfile, string) (CommentItem, error) { return CommentItem{}, s.err } func (s unavailableStore) CountComments(context.Context) (int, error) { return 0, s.err } func (s unavailableStore) ListAudit(context.Context) ([]AuditItem, error) { return nil, s.err }