Files
backend_fiesta/db/connect.go
Suriyakumarvijayanayagam 4474479735 Load .env.<APP_ENV>, validate config at boot, keep secrets out of the image
`main.go` only ever loaded `.env`; the `APP_ENV` switch described in
`.env.local` / `.env.production` did not exist, and a missing variable
surfaced one restart at a time as a log.Fatalf inside db.Connect.

config.Load now picks `.env.<APP_ENV>` (default local) then `.env`, with
real environment winning, reads every setting into one typed Config and
reports everything missing in one message. Production insists on a POS
signing secret; local warns when DB_HOST is not a local address. db,
redis and the image store take the Config instead of reading env
themselves.

Also:
- livehub read MQTT_USERNAME while everything else uses MQTT_USER, so the
  console stream connected to the broker unauthenticated. Both accepted.
- .dockerignore: `COPY . .` was baking .env.production into the image.
  Dockerfile sets APP_ENV=production.
- Drop utils/config.go (dead viper loader) and create_table.go (unused,
  hardcoded production DSN); go mod tidy removes viper.
- .env.example lists every variable the code reads; docs/ENVIRONMENT.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 17:04:33 +05:30

114 lines
2.8 KiB
Go

package db
import (
"fmt"
"log"
"nearle/config"
"net/url"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var (
DB *gorm.DB
// CatalogueDB is a separate connection to the pgvector/catalogue database.
// It is intentionally isolated from DB (nearledb) so catalogue integration
// never touches the main database.
CatalogueDB *gorm.DB
)
// --------------------
// DATABASE CONNECTION
// --------------------
// Connect opens the main database and then the optional catalogue database
// and image store. Everything it needs has already been validated by
// config.Load, so a missing variable can no longer surface here as a
// log.Fatal halfway through boot.
func Connect(cfg *config.Config) {
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Kolkata",
cfg.DB.Host,
cfg.DB.User,
cfg.DB.Password,
cfg.DB.Name,
cfg.DB.Port,
)
var err error
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("❌ Could not connect to database:", err)
}
setupDB(DB)
fmt.Println("✅ Database connected")
connectCatalogueDB(cfg.Catalogue)
connectImageStore(cfg.S3)
}
// connectCatalogueDB opens the read-only connection to the catalogue
// (pgvector) database. If its env vars are not set, catalogue endpoints
// are simply unavailable — this must never block startup of the main app.
func connectCatalogueDB(c config.DBConfig) {
if !c.Enabled() {
fmt.Println("⚠️ CATALOGUE_DB_HOST not set, skipping catalogue DB connection")
return
}
// Built as a URL (not a keyword=value DSN) and percent-encoded via
// net/url, because this credential's password contains characters
// (quotes, #) that the keyword=value DSN format would misparse as
// quoting/comment syntax.
dsnURL := url.URL{
Scheme: "postgres",
User: url.UserPassword(c.User, c.Password),
Host: fmt.Sprintf("%s:%s", c.Host, c.Port),
Path: "/" + c.Name,
}
q := dsnURL.Query()
q.Set("sslmode", "disable")
dsnURL.RawQuery = q.Encode()
catalogueDB, err := gorm.Open(postgres.Open(dsnURL.String()), &gorm.Config{})
if err != nil {
log.Println("❌ Could not connect to catalogue database:", err)
return
}
setupDB(catalogueDB)
CatalogueDB = catalogueDB
fmt.Println("✅ Catalogue database connected")
}
func setupDB(database *gorm.DB) {
sqlDB, err := database.DB()
if err != nil {
log.Fatal("❌ Failed to get DB from GORM:", err)
}
sqlDB.SetMaxIdleConns(100)
sqlDB.SetMaxOpenConns(1000)
sqlDB.SetConnMaxLifetime(time.Minute * 5)
}
// --------------------
// DATABASE SHUTDOWN
// --------------------
func CloseDB() {
if DB != nil {
if sqlDB, err := DB.DB(); err == nil {
sqlDB.Close()
}
}
if CatalogueDB != nil {
if sqlDB, err := CatalogueDB.DB(); err == nil {
sqlDB.Close()
}
}
fmt.Println("Connection closed Successfully")
}