refactor(10.2-01): nest framework packages under modules
- Move remaining beach packages and embedded admin assets\n- Rewrite framework, example, build, and gate paths
This commit is contained in:
146
modules/lagoon/connection.go
Normal file
146
modules/lagoon/connection.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package lagoon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
requiredLocaleProvider = "i"
|
||||
requiredICULocale = "pl-PL"
|
||||
)
|
||||
|
||||
// Open pings dsn through pgx stdlib, requires Postgres 16 ICU pl-PL, and
|
||||
// returns that exact *sql.DB plus a GORM handle opened on it.
|
||||
//
|
||||
// Phase 11 owns a separate pgxpool.Pool for River LISTEN/NOTIFY. Do not
|
||||
// create that listener pool here; application queries share this *sql.DB.
|
||||
func Open(ctx context.Context, dsn string) (*sql.DB, *gorm.DB, error) {
|
||||
dsn = strings.TrimSpace(dsn)
|
||||
if dsn == "" {
|
||||
return nil, nil, fmt.Errorf("lagoon: database.dsn is empty (set SUMMER_DATABASE__DSN)")
|
||||
}
|
||||
sqlDB, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("lagoon: open postgres: %w", err)
|
||||
}
|
||||
if err := sqlDB.PingContext(ctx); err != nil {
|
||||
_ = sqlDB.Close()
|
||||
return nil, nil, fmt.Errorf("lagoon: ping postgres: %w", err)
|
||||
}
|
||||
if err := CheckLocale(ctx, sqlDB); err != nil {
|
||||
_ = sqlDB.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
gdb, err := gormFromSQL(sqlDB)
|
||||
if err != nil {
|
||||
_ = sqlDB.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
return sqlDB, gdb, nil
|
||||
}
|
||||
|
||||
// Use pings an existing pool, requires ICU pl-PL, and returns a GORM handle
|
||||
// opened on that exact *sql.DB. Callers that already hold a pool (tests,
|
||||
// the app boot seam) must not open a second connection.
|
||||
func Use(ctx context.Context, sqlDB *sql.DB) (*gorm.DB, error) {
|
||||
if sqlDB == nil {
|
||||
return nil, fmt.Errorf("lagoon: sql db is nil")
|
||||
}
|
||||
if err := sqlDB.PingContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("lagoon: ping postgres: %w", err)
|
||||
}
|
||||
if err := CheckLocale(ctx, sqlDB); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gormFromSQL(sqlDB)
|
||||
}
|
||||
|
||||
func gormFromSQL(sqlDB *sql.DB) (*gorm.DB, error) {
|
||||
gdb, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lagoon: gorm open: %w", err)
|
||||
}
|
||||
got, err := gdb.DB()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lagoon: gorm sql handle: %w", err)
|
||||
}
|
||||
if got != sqlDB {
|
||||
return nil, fmt.Errorf("lagoon: GORM is not using the shared *sql.DB")
|
||||
}
|
||||
return gdb, nil
|
||||
}
|
||||
|
||||
// OpenFromApp reads database.dsn from app config and opens the shared pool.
|
||||
// It also loads app.key via LoadAppKey and PublishEncryptionKeys so Encrypted
|
||||
// columns do not re-read config on every row.
|
||||
func OpenFromApp(ctx context.Context, app *backpack.App) (*sql.DB, *gorm.DB, error) {
|
||||
if app == nil || app.Config == nil {
|
||||
return nil, nil, fmt.Errorf("lagoon: app config is missing")
|
||||
}
|
||||
sqlDB, gdb, err := Open(ctx, DSN(app.Config))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := loadEncryptionKeysFromApp(app); err != nil {
|
||||
_ = sqlDB.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
return sqlDB, gdb, nil
|
||||
}
|
||||
|
||||
// DSN returns database.dsn from layered config (env SUMMER_DATABASE__DSN).
|
||||
func DSN(cfg *compass.Config) string {
|
||||
if cfg == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(cfg.String("database.dsn"))
|
||||
}
|
||||
|
||||
// Publish stores the shared SQL pool and GORM handle on the app. Both
|
||||
// handles refer to the same *sql.DB.
|
||||
func Publish(app *backpack.App, sqlDB *sql.DB, gdb *gorm.DB) error {
|
||||
if app == nil {
|
||||
return fmt.Errorf("lagoon: app is nil")
|
||||
}
|
||||
if sqlDB == nil || gdb == nil {
|
||||
return fmt.Errorf("lagoon: database handles are nil")
|
||||
}
|
||||
if err := app.Publish(sqlDB); err != nil {
|
||||
return err
|
||||
}
|
||||
return app.Publish(gdb)
|
||||
}
|
||||
|
||||
// CheckLocale fails unless the connected database uses ICU locale pl-PL.
|
||||
func CheckLocale(ctx context.Context, db *sql.DB) error {
|
||||
if db == nil {
|
||||
return fmt.Errorf("lagoon: sql db is nil")
|
||||
}
|
||||
var provider, icu string
|
||||
err := db.QueryRowContext(ctx, `
|
||||
SELECT datlocprovider::text, COALESCE(daticulocale, '')
|
||||
FROM pg_database
|
||||
WHERE datname = current_database()`).Scan(&provider, &icu)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lagoon: read database locale: %w", err)
|
||||
}
|
||||
return checkLocale(provider, icu)
|
||||
}
|
||||
|
||||
func checkLocale(provider, icu string) error {
|
||||
provider = strings.TrimSpace(provider)
|
||||
icu = strings.TrimSpace(icu)
|
||||
if provider == requiredLocaleProvider && icu == requiredICULocale {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("lagoon: database locale must be ICU pl-PL (datlocprovider=%q, daticulocale=%q); got provider %q locale %q. Create the database with: CREATE DATABASE ... TEMPLATE template0 ENCODING 'UTF8' LOCALE_PROVIDER icu ICU_LOCALE 'pl-PL'", requiredLocaleProvider, requiredICULocale, provider, icu)
|
||||
}
|
||||
Reference in New Issue
Block a user