From 7afe1f279e15ad1b7ac83b7a18169f7b72a562b7 Mon Sep 17 00:00:00 2001 From: Primadi Setiawan Date: Tue, 16 Dec 2025 02:00:59 +0700 Subject: [PATCH] refactor lokstra_init --- cmd/lokstra/main.go | 4 +- cmd/lokstra/migration.go | 3 +- .../internal/multifile_test/main.go | 6 +- core/app/listener/fasthttp/fasthttp.go | 4 +- core/app/listener/net_http.go | 4 +- core/deploy/loader/builder.go | 55 +-- core/deploy/loader/loader.go | 63 +-- core/deploy/loader/test/loader_test.go | 2 +- core/deploy/registry.go | 6 + core/deploy/schema/schema.go | 3 +- core/service/lazy_load.go | 29 -- core/service/lazy_load_test.go | 65 --- dbpool_manager/dbpool_crud.go | 162 +++++++ dbpool_manager/dbpool_crud_test.go | 150 ++++++ .../01_enterprise_router_service/main.go | 8 +- .../02-multi-deployment-yaml/main.go | 6 +- .../04-external-services/main.go | 5 +- .../full-framework/05-remote-router/main.go | 2 +- .../06-inline-definitions-example/main.go | 6 +- .../examples/01-basic-config/README.md | 2 +- .../examples/01-basic-config/main.go | 8 +- .../04-config/examples/02-multi-file/main.go | 9 +- .../04-config/examples/06-handlers/main.go | 8 +- docs/02-framework-guide/08-database-pools.md | 6 +- docs/BOOTSTRAP-FLOWS.md | 441 ------------------ docs/CONFIG-FIRST-IMPLEMENTATION.md | 266 ----------- lokstra_helper.go | 7 - .../bootstrap.go | 28 +- lokstra_init/initialize.go | 160 +++++++ .../migration.go | 153 ++---- .../migration_test.go | 44 +- lokstra_init/option.go | 95 ++++ .../pgx_dbpool_manager.go | 2 +- lokstra_init_option.go | 96 ---- lokstra_initialize.go | 150 ------ lokstra_registry/helper.go | 58 +-- lokstra_registry/registry.go | 5 + loksttra_pgx_sync_config.go | 8 - .../02_app_framework/01_medium_system/main.go | 6 +- .../02_enterprise_modular/main.go | 6 +- .../README_FLOWS.md | 2 +- .../03_enterprise_router_service/main.go | 8 +- .../03_enterprise_router_service/main_alt.go | 9 +- .../04_sync_config/config/config.yaml | 7 - .../02_app_framework/04_sync_config/main.go | 33 +- .../02_app_framework/test/main.go | 7 +- serviceapi/dbpool_manager.go | 10 + services/dbpool_manager/dbpool_manager.go | 43 +- services/dbpool_manager/sync_pool_manager.go | 58 ++- services/dbpool_pg/module.go | 7 +- services/email_smtp/example/README.md | 4 +- services/email_smtp/example/main.go | 5 +- services/register_all.go | 4 +- services/sync_config_pg/config_test.yaml | 7 - services/sync_config_pg/module.go | 22 +- services/sync_config_pg/module_test.go | 4 +- services/sync_config_pg/test/main.go | 16 +- syncmap/syncmap.go | 16 +- syncmap/syncmap_test.go | 4 +- .../example/example_multi_db.go | 19 +- .../example/example_with_lokstra.go | 38 +- tools/migration_runner/example/main.go | 6 +- 62 files changed, 931 insertions(+), 1539 deletions(-) create mode 100644 dbpool_manager/dbpool_crud.go create mode 100644 dbpool_manager/dbpool_crud_test.go delete mode 100644 docs/BOOTSTRAP-FLOWS.md delete mode 100644 docs/CONFIG-FIRST-IMPLEMENTATION.md rename lokstra_bootstrap.go => lokstra_init/bootstrap.go (87%) create mode 100644 lokstra_init/initialize.go rename lokstra_migration.go => lokstra_init/migration.go (68%) rename migration_test.go => lokstra_init/migration_test.go (80%) create mode 100644 lokstra_init/option.go rename lokstra_pgx_dbpool_manager.go => lokstra_init/pgx_dbpool_manager.go (97%) delete mode 100644 lokstra_init_option.go delete mode 100644 lokstra_initialize.go delete mode 100644 loksttra_pgx_sync_config.go diff --git a/cmd/lokstra/main.go b/cmd/lokstra/main.go index 8827fdee..95a55672 100644 --- a/cmd/lokstra/main.go +++ b/cmd/lokstra/main.go @@ -7,10 +7,10 @@ import ( "path/filepath" "slices" - "github.com/primadi/lokstra" "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/core/annotation" + "github.com/primadi/lokstra/lokstra_init" ) const version = "1.0.2" @@ -19,7 +19,7 @@ func main() { logger.SetLogLevel(logger.LogLevelInfo) // for debugging purpose - if lokstra.DetectRunMode() != lokstra.RunModeProd { + if lokstra_init.DetectRunMode() != lokstra_init.RunModeProd { // use 04_sync_config template for testing os.Chdir(filepath.Join(utils.GetBasePath(), "../../project_templates/02_app_framework/04_sync_config")) // os.Args = slices.Concat(os.Args[:1], []string{"migration", "status"}) diff --git a/cmd/lokstra/migration.go b/cmd/lokstra/migration.go index 2c22b8f9..8db4167b 100644 --- a/cmd/lokstra/migration.go +++ b/cmd/lokstra/migration.go @@ -8,7 +8,6 @@ import ( "path/filepath" "time" - "github.com/primadi/lokstra" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/serviceapi" @@ -124,7 +123,7 @@ func executeMigration(subCmd, configFile, migrationDir, dbPoolName string, steps } // load named-db-pools from config file - if err := lokstra.LoadConfig(cfgFile); err != nil { + if err := lokstra_registry.LoadConfig(cfgFile); err != nil { return fmt.Errorf("failed to load config file '%s': %w", filepath.Base(cfgFile), err) } diff --git a/core/annotation/internal/multifile_test/main.go b/core/annotation/internal/multifile_test/main.go index f3bab53a..1de69a17 100644 --- a/core/annotation/internal/multifile_test/main.go +++ b/core/annotation/internal/multifile_test/main.go @@ -1,9 +1,7 @@ package main -import ( - "github.com/primadi/lokstra" -) +import "github.com/primadi/lokstra/lokstra_init" func main() { - lokstra.Bootstrap() + lokstra_init.Bootstrap() } diff --git a/core/app/listener/fasthttp/fasthttp.go b/core/app/listener/fasthttp/fasthttp.go index 4fbff98a..8cb0197f 100644 --- a/core/app/listener/fasthttp/fasthttp.go +++ b/core/app/listener/fasthttp/fasthttp.go @@ -71,14 +71,14 @@ func (s *FastHttp) ListenAndServe() error { if err != nil { return fmt.Errorf("failed to listen on unix socket: %w", err) } - logger.LogInfo("[FastHttp] Starting server on Unix socket %s\n", socketPath) + // logger.LogInfo("[FastHttp] Starting server on Unix socket %s\n", socketPath) } else { var err error listener, err = net.Listen("tcp", s.addr) if err != nil { return listener_utils.WrapListenError(s.addr, err) } - logger.LogInfo("[FastHttp] Starting server on TCP %s\n", s.addr) + // logger.LogInfo("[FastHttp] Starting server on TCP %s\n", s.addr) } if s.secure { diff --git a/core/app/listener/net_http.go b/core/app/listener/net_http.go index 448653d2..3b08b48f 100644 --- a/core/app/listener/net_http.go +++ b/core/app/listener/net_http.go @@ -78,14 +78,14 @@ func (s *NetHttp) ListenAndServe() error { if err != nil { return fmt.Errorf("failed to listen on unix socket: %w", err) } - logger.LogInfo("[NETHTTP] Starting server on Unix socket %s\n", socketPath) + // logger.LogInfo("[NETHTTP] Starting server on Unix socket %s\n", socketPath) } else { var err error listener, err = net.Listen("tcp", s.server.Addr) if err != nil { return listener_utils.WrapListenError(s.server.Addr, err) } - logger.LogInfo("[NETHTTP] Starting server on TCP %s\n", s.server.Addr) + // logger.LogInfo("[NETHTTP] Starting server on TCP %s\n", s.server.Addr) } if s.secure { diff --git a/core/deploy/loader/builder.go b/core/deploy/loader/builder.go index 3388d602..baaaf438 100644 --- a/core/deploy/loader/builder.go +++ b/core/deploy/loader/builder.go @@ -2,8 +2,6 @@ package loader import ( "fmt" - "os" - "path/filepath" "strings" "time" @@ -859,12 +857,12 @@ func RegisterDefinitionsForRuntime(registry *deploy.GlobalRegistry, config *sche return nil } -// LoadAndBuild loads config and builds ALL deployments into Global registry +// LoadConfig loads config and builds ALL deployments into Global registry // Returns error only - deployments are stored in deploy.Global() -func LoadAndBuild(configPaths []string) error { - config, err := LoadConfig(configPaths...) +func LoadConfig(configPaths ...string) (*schema.DeployConfig, error) { + config, err := loadConfig(configPaths...) if err != nil { - return fmt.Errorf("failed to load config: %w", err) + return nil, fmt.Errorf("failed to load config: %w", err) } registry := deploy.Global() @@ -878,7 +876,7 @@ func LoadAndBuild(configPaths []string) error { // Store definitions to registry (NO runtime registration, just store data) // Runtime registration will happen in RunCurrentServer if err := StoreDefinitionsToRegistry(registry, config); err != nil { - return fmt.Errorf("failed to store definitions: %w", err) + return nil, fmt.Errorf("failed to store definitions: %w", err) } // Build ALL deployments (2-Layer Architecture: YAML -> Topology only) @@ -978,7 +976,7 @@ func LoadAndBuild(configPaths []string) error { // return fmt.Errorf("failed to setup named DB pools: %w", err) // } - return nil + return config, nil } // LoadNamedDbPoolsFromConfig auto-discovers and sets up named DB pools from config @@ -1084,48 +1082,11 @@ func LoadNamedDbPoolsFromConfig() error { } // Set DSN and Schema for poolName - dbPoolManager.SetNamedDbPool(poolName, dsn, schema, nil) - - // Create the pool - dbPool, err := dbPoolManager.GetNamedDbPool(poolName) - if err != nil { - return fmt.Errorf("failed to create pool '%s': %w", poolName, err) - } - - // Register pool as a service - registry.RegisterService(poolName, dbPool) + // This also auto-registers the pool as a lazy service + dbPoolManager.SetNamedDbPool(poolName, dsn, schema, poolConfig.RlsContext) logger.LogDebug("✅ Registered DB pool: %s (schema: %s)", poolName, schema) } return nil } - -// LoadAndBuildFromDir loads all YAML files from a directory and builds ALL deployments -func LoadAndBuildFromDir(dirPath string) error { - // Scan directory for YAML files - entries, err := os.ReadDir(dirPath) - if err != nil { - return fmt.Errorf("failed to read directory: %w", err) - } - - var paths []string - for _, entry := range entries { - if entry.IsDir() { - continue - } - - name := entry.Name() - ext := filepath.Ext(name) - if ext == ".yaml" || ext == ".yml" { - paths = append(paths, filepath.Join(dirPath, name)) - } - } - - if len(paths) == 0 { - return fmt.Errorf("no YAML files found in directory: %s", dirPath) - } - - // Delegate to LoadAndBuild - return LoadAndBuild(paths) -} diff --git a/core/deploy/loader/loader.go b/core/deploy/loader/loader.go index 4e0126ad..e8295e21 100644 --- a/core/deploy/loader/loader.go +++ b/core/deploy/loader/loader.go @@ -15,9 +15,11 @@ import ( "gopkg.in/yaml.v3" ) -// LoadConfig loads a deployment configuration from YAML file(s) +// loadConfig loads a deployment configuration from YAML file(s) // Supports single file or multiple files that will be merged -func LoadConfig(paths ...string) (*schema.DeployConfig, error) { +// Paths can be files or folders - folders will be expanded to all *.yaml files +// This is a private function - external code should use LoadAndBuild instead +func loadConfig(paths ...string) (*schema.DeployConfig, error) { if len(paths) == 0 { return nil, fmt.Errorf("no config files specified") } @@ -26,16 +28,42 @@ func LoadConfig(paths ...string) (*schema.DeployConfig, error) { basePath := utils.GetBasePath() - // STEP 1: Load and merge all files (RAW, no resolution yet) + // STEP 0: Expand folders to files + var expandedPaths []string for _, path := range paths { // If path is already absolute, use it directly; otherwise join with basePath normPath := path if !filepath.IsAbs(path) { normPath = filepath.Join(basePath, path) } + + // Check if path is a directory + info, err := os.Stat(normPath) + if err != nil { + return nil, fmt.Errorf("failed to access %s: %w", path, err) + } + + if info.IsDir() { + // Expand directory to *.yaml files + yamlFiles, err := filepath.Glob(filepath.Join(normPath, "*.yaml")) + if err != nil { + return nil, fmt.Errorf("failed to scan directory %s: %w", path, err) + } + if len(yamlFiles) == 0 { + return nil, fmt.Errorf("no YAML files found in directory: %s", path) + } + expandedPaths = append(expandedPaths, yamlFiles...) + } else { + // It's a file, use as is + expandedPaths = append(expandedPaths, normPath) + } + } + + // STEP 1: Load and merge all files (RAW, no resolution yet) + for _, normPath := range expandedPaths { config, err := loadSingleFileRaw(normPath) if err != nil { - return nil, fmt.Errorf("failed to load %s: %w", path, err) + return nil, fmt.Errorf("failed to load %s: %w", normPath, err) } if merged == nil { @@ -244,30 +272,3 @@ func ValidateConfig(config *schema.DeployConfig) error { return nil } - -// LoadConfigFromDir loads all .yaml and .yml files from a directory and merges them -func LoadConfigFromDir(dirPath string) (*schema.DeployConfig, error) { - entries, err := os.ReadDir(dirPath) - if err != nil { - return nil, fmt.Errorf("failed to read directory: %w", err) - } - - var paths []string - for _, entry := range entries { - if entry.IsDir() { - continue - } - - name := entry.Name() - ext := filepath.Ext(name) - if ext == ".yaml" || ext == ".yml" { - paths = append(paths, filepath.Join(dirPath, name)) - } - } - - if len(paths) == 0 { - return nil, fmt.Errorf("no YAML files found in directory: %s", dirPath) - } - - return LoadConfig(paths...) -} diff --git a/core/deploy/loader/test/loader_test.go b/core/deploy/loader/test/loader_test.go index 61e866da..967a7cb6 100644 --- a/core/deploy/loader/test/loader_test.go +++ b/core/deploy/loader/test/loader_test.go @@ -109,7 +109,7 @@ func TestLoadMultipleFiles(t *testing.T) { } func TestLoadFromDirectory(t *testing.T) { - config, err := loader.LoadConfigFromDir("testdata") + config, err := loader.LoadConfig("testdata") if err != nil { t.Fatalf("failed to load from directory: %v", err) } diff --git a/core/deploy/registry.go b/core/deploy/registry.go index 960d145a..84ec2dc5 100644 --- a/core/deploy/registry.go +++ b/core/deploy/registry.go @@ -837,6 +837,12 @@ func (g *GlobalRegistry) RegisterService(name string, service any) { logger.LogDebug("ℹ️ Registered service instance: '%s'\n", name) } +// UnregisterService removes a service instance from the registry +func (g *GlobalRegistry) UnregisterService(name string) { + g.serviceInstances.Delete(name) + logger.LogDebug("ℹ️ Unregistered service instance: '%s'\n", name) +} + // RegisterLazyService registers a lazy service factory that will be instantiated on first access. // The factory will be called only once, and the result is cached. // This allows services to be registered in any order, regardless of dependencies. diff --git a/core/deploy/schema/schema.go b/core/deploy/schema/schema.go index deaa297c..3f8ba12b 100644 --- a/core/deploy/schema/schema.go +++ b/core/deploy/schema/schema.go @@ -38,7 +38,8 @@ type DbPoolConfig struct { Password string `yaml:"password,omitempty" json:"password,omitempty"` // Schema configuration - Schema string `yaml:"schema,omitempty" json:"schema,omitempty"` // Default: "public" + Schema string `yaml:"schema,omitempty" json:"schema,omitempty"` // Default: "public" + RlsContext map[string]string `yaml:"rls-context,omitempty" json:"rls-context,omitempty"` // Row Level Security context variables // Pool configuration (optional) MinConns int `yaml:"min-conns,omitempty" json:"min-conns,omitempty"` diff --git a/core/service/lazy_load.go b/core/service/lazy_load.go index 836d694c..5888e5c8 100644 --- a/core/service/lazy_load.go +++ b/core/service/lazy_load.go @@ -107,35 +107,6 @@ func (l *Cached[T]) IsLoaded() bool { return !utils.IsNil(l.cache) } -// creates a lazy service loader from factory service configuration map. -func LazyLoadFromConfig[T any](cfg map[string]any, key string) *Cached[T] { - if cfg == nil { - return nil - } - - val, ok := cfg[key] - if !ok { - return nil - } - - if svcName, ok := val.(string); ok { - return LazyLoad[T](svcName) - } - - // Not a valid service reference - return nil -} - -// creates a lazy service loader from factory service configuration map. -// It panics if the key is missing or invalid. -func MustLazyLoadFromConfig[T any](cfg map[string]any, key string) *Cached[T] { - lazy := LazyLoadFromConfig[T](cfg, key) - if lazy == nil { - panic("missing required dependency '" + key + "'") - } - return lazy -} - // LazyLoadWith creates a lazy service loader with a custom loader function. // The loader function is called on first Get() and the result is cached. // This is useful for dependency injection frameworks that manage their own service resolution. diff --git a/core/service/lazy_load_test.go b/core/service/lazy_load_test.go index 1df09284..1372cae2 100644 --- a/core/service/lazy_load_test.go +++ b/core/service/lazy_load_test.go @@ -152,68 +152,3 @@ func TestCast(t *testing.T) { t.Errorf("expected name 'cast-test', got '%s'", retrieved.Name) } } - -func TestLazyLoadFromConfig(t *testing.T) { - _ = deploy.Global() - - testSvc := &TestService{Name: "config-test"} - lokstra_registry.RegisterService("db-service", testSvc) - - config := map[string]any{ - "database": "db-service", - "timeout": 30, - } - - lazy := service.LazyLoadFromConfig[*TestService](config, "database") - if lazy == nil { - t.Fatal("expected lazy loader to be created") - } - - retrieved := lazy.Get() - if retrieved.Name != "config-test" { - t.Errorf("expected name 'config-test', got '%s'", retrieved.Name) - } -} - -func TestLazyLoadFromConfig_MissingKey(t *testing.T) { - config := map[string]any{ - "timeout": 30, - } - - lazy := service.LazyLoadFromConfig[*TestService](config, "database") - if lazy != nil { - t.Error("expected nil when key is missing") - } -} - -func TestMustLazyLoadFromConfig_Success(t *testing.T) { - _ = deploy.Global() - - testSvc := &TestService{Name: "must-config"} - lokstra_registry.RegisterService("cache-service", testSvc) - - config := map[string]any{ - "cache": "cache-service", - } - - lazy := service.MustLazyLoadFromConfig[*TestService](config, "cache") - retrieved := lazy.Get() - - if retrieved.Name != "must-config" { - t.Errorf("expected name 'must-config', got '%s'", retrieved.Name) - } -} - -func TestMustLazyLoadFromConfig_Panic(t *testing.T) { - config := map[string]any{ - "timeout": 30, - } - - defer func() { - if r := recover(); r == nil { - t.Error("expected panic when required key is missing") - } - }() - - service.MustLazyLoadFromConfig[*TestService](config, "database") -} diff --git a/dbpool_manager/dbpool_crud.go b/dbpool_manager/dbpool_crud.go new file mode 100644 index 00000000..baa35dda --- /dev/null +++ b/dbpool_manager/dbpool_crud.go @@ -0,0 +1,162 @@ +package dbpool_manager + +import ( + "context" + "fmt" + + "github.com/primadi/lokstra/common/logger" + "github.com/primadi/lokstra/core/service" + "github.com/primadi/lokstra/serviceapi" +) + +// DbPoolConfig represents database pool configuration +type DbPoolConfig struct { + Name string `json:"name"` + DSN string `json:"dsn"` + Schema string `json:"schema"` + RlsContext map[string]string `json:"rls_context,omitempty"` +} + +var dbpm = service.LazyLoad[serviceapi.DbPoolManager]("dbpool-manager") + +// AddDbPool adds a new database pool at runtime +// This will persist to sync_config database if using distributed sync +// Note: If pool already exists, it will be updated (upsert behavior) +func AddDbPool(config DbPoolConfig) error { + if config.Name == "" { + return fmt.Errorf("pool name is required") + } + if config.DSN == "" { + return fmt.Errorf("DSN is required") + } + if config.Schema == "" { + config.Schema = "public" // Default schema + } + + dpm := dbpm.Get() + if dpm == nil { + return fmt.Errorf("dbpool-manager service not found") + } + + // Set the pool configuration (upsert: works for both new and existing pools) + // This also auto-registers the pool as a lazy service + dpm.SetNamedDbPool(config.Name, config.DSN, config.Schema, config.RlsContext) + + // Validate configuration by attempting to get the pool + _, err := dpm.GetNamedDbPool(config.Name) + if err != nil { + // Rollback on validation failure + dpm.RemoveNamedDbPool(config.Name) + return fmt.Errorf("failed to create pool '%s': %w", config.Name, err) + } + + logger.LogInfo("✅ Added/Updated DB pool: %s (schema: %s)", config.Name, config.Schema) + return nil +} + +// UpdateDbPool updates an existing database pool configuration +// This is an alias for AddDbPool (both do upsert) +// Note: This will NOT close existing connections, new connections will use new config +func UpdateDbPool(config DbPoolConfig) error { + return AddDbPool(config) // Same implementation - upsert behavior +} + +// RemoveDbPool removes a database pool at runtime +// This will persist to sync_config database if using distributed sync +// The pool service will also be unregistered from the service registry +func RemoveDbPool(name string) error { + if name == "" { + return fmt.Errorf("pool name is required") + } + + dpm := dbpm.Get() + if dpm == nil { + return fmt.Errorf("dbpool-manager service not found") + } + + // Check if pool exists + _, _, _, err := dpm.GetNamedDbPoolInfo(name) + if err != nil { + return fmt.Errorf("pool '%s' not found: %w", name, err) + } + + // Remove from manager (also unregisters service automatically) + dpm.RemoveNamedDbPool(name) + + logger.LogInfo("✅ Removed DB pool: %s", name) + return nil +} + +// GetDbPoolInfo retrieves database pool configuration +func GetDbPoolInfo(name string) (*DbPoolConfig, error) { + if name == "" { + return nil, fmt.Errorf("pool name is required") + } + + dpm := dbpm.Get() + if dpm == nil { + return nil, fmt.Errorf("dbpool-manager service not found") + } + + dsn, schema, rlsContext, err := dpm.GetNamedDbPoolInfo(name) + if err != nil { + return nil, fmt.Errorf("pool '%s' not found: %w", name, err) + } + + return &DbPoolConfig{ + Name: name, + DSN: dsn, + Schema: schema, + RlsContext: rlsContext, + }, nil +} + +// ListDbPools returns all configured database pools +// Works with both regular and sync DbPoolManager +func ListDbPools() ([]string, error) { + dpm := dbpm.Get() + if dpm == nil { + return nil, fmt.Errorf("dbpool-manager service not found") + } + + // Use GetAllNamedDbPools from DbPoolManager interface + allPools := dpm.GetAllNamedDbPools() + if allPools == nil { + return []string{}, nil + } + + poolNames := make([]string, 0, len(allPools)) + for poolName := range allPools { + poolNames = append(poolNames, poolName) + } + + return poolNames, nil +} + +// GetDbPool returns the actual DbPool instance for direct use +func GetDbPool(name string) (serviceapi.DbPool, error) { + if name == "" { + return nil, fmt.Errorf("pool name is required") + } + + dpm := dbpm.Get() + if dpm == nil { + return nil, fmt.Errorf("dbpool-manager service not found") + } + + return dpm.GetNamedDbPool(name) +} + +// AcquireDbConn acquires a connection from a named pool +func AcquireDbConn(ctx context.Context, poolName string) (serviceapi.DbConn, error) { + if poolName == "" { + return nil, fmt.Errorf("pool name is required") + } + + dpm := dbpm.Get() + if dpm == nil { + return nil, fmt.Errorf("dbpool-manager service not found") + } + + return dpm.AcquireNamedConn(ctx, poolName) +} diff --git a/dbpool_manager/dbpool_crud_test.go b/dbpool_manager/dbpool_crud_test.go new file mode 100644 index 00000000..c0b335e7 --- /dev/null +++ b/dbpool_manager/dbpool_crud_test.go @@ -0,0 +1,150 @@ +package dbpool_manager_test + +import ( + "context" + "testing" + + "github.com/primadi/lokstra/dbpool_manager" + "github.com/primadi/lokstra/lokstra_init" +) + +// Example: Add a new database pool at runtime +func ExampleAddDbPool() { + // Initialize framework first + lokstra_init.UsePgxDbPoolManager(true) // Enable distributed sync + + // Add new pool + err := dbpool_manager.AddDbPool(dbpool_manager.DbPoolConfig{ + Name: "db-analytics", + DSN: "postgres://user:pass@localhost:5432/analytics", + Schema: "public", + }) + + if err != nil { + panic(err) + } + + // Pool is now available across all servers (if using distributed sync) + conn, _ := dbpool_manager.AcquireDbConn(context.Background(), "db-analytics") + defer conn.Release() + + // Use connection... +} + +// Example: Update existing pool configuration +func ExampleUpdateDbPool() { + err := dbpool_manager.UpdateDbPool(dbpool_manager.DbPoolConfig{ + Name: "db-main", + DSN: "postgres://user:pass@new-host:5432/main", + Schema: "public", + }) + + if err != nil { + panic(err) + } + + // New connections will use the updated configuration + // Existing connections remain unchanged until closed +} + +// Example: Remove a pool +func ExampleRemoveDbPool() { + err := dbpool_manager.RemoveDbPool("db-old-tenant") + if err != nil { + panic(err) + } + + // Pool removed from all servers (if using distributed sync) +} + +// Example: List all pools +func ExampleListDbPools() { + pools, err := dbpool_manager.ListDbPools() + if err != nil { + panic(err) + } + + for _, poolName := range pools { + info, _ := dbpool_manager.GetDbPoolInfo(poolName) + println("Pool:", info.Name, "Schema:", info.Schema) + } +} + +// Example: Get pool info +func ExampleGetDbPoolInfo() { + info, err := dbpool_manager.GetDbPoolInfo("db-main") + if err != nil { + panic(err) + } + + println("DSN:", info.DSN) + println("Schema:", info.Schema) +} + +// Example: Direct pool access +func ExampleGetDbPool() { + pool, err := dbpool_manager.GetDbPool("db-main") + if err != nil { + panic(err) + } + + conn, _ := pool.Acquire(context.Background()) + defer conn.Release() + + // Use connection... +} + +// Test CRUD operations +func TestDbPoolCRUD(t *testing.T) { + // Setup + lokstra_init.UsePgxDbPoolManager(false) // Use local sync for testing + + // Create + err := dbpool_manager.AddDbPool(dbpool_manager.DbPoolConfig{ + Name: "test-pool", + DSN: "postgres://localhost/test", + Schema: "test_schema", + }) + if err != nil { + t.Fatalf("Failed to add pool: %v", err) + } + + // Read + info, err := dbpool_manager.GetDbPoolInfo("test-pool") + if err != nil { + t.Fatalf("Failed to get pool info: %v", err) + } + if info.Name != "test-pool" { + t.Errorf("Expected name 'test-pool', got '%s'", info.Name) + } + if info.Schema != "test_schema" { + t.Errorf("Expected schema 'test_schema', got '%s'", info.Schema) + } + + // Update + err = dbpool_manager.UpdateDbPool(dbpool_manager.DbPoolConfig{ + Name: "test-pool", + DSN: "postgres://localhost/test2", + Schema: "test_schema2", + }) + if err != nil { + t.Fatalf("Failed to update pool: %v", err) + } + + info, _ = dbpool_manager.GetDbPoolInfo("test-pool") + if info.Schema != "test_schema2" { + t.Errorf("Expected schema 'test_schema2', got '%s'", info.Schema) + } + + // Delete + err = dbpool_manager.RemoveDbPool("test-pool") + if err != nil { + t.Fatalf("Failed to remove pool: %v", err) + } + + // Verify deletion + _, err = dbpool_manager.GetDbPoolInfo("test-pool") + if err == nil { + t.Error("Expected error when getting deleted pool") + } +} diff --git a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/main.go b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/main.go index 87bcccb1..6f60226e 100644 --- a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/main.go +++ b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/main.go @@ -1,13 +1,13 @@ package main import ( - "github.com/primadi/lokstra" "github.com/primadi/lokstra/common/logger" + "github.com/primadi/lokstra/lokstra_init" "github.com/primadi/lokstra/lokstra_registry" ) func main() { - lokstra.Bootstrap() + lokstra_init.Bootstrap() logger.LogInfo("") logger.LogInfo("╔═══════════════════════════════════════════════╗") @@ -18,7 +18,7 @@ func main() { logger.SetLogLevelFromEnv() - lokstra.LoadConfigFromFolder("config") + lokstra_registry.LoadConfig("config") dsn := lokstra_registry.GetConfig("db_main.dsn", "") schema := lokstra_registry.GetConfig("db_main.schema", "public") @@ -42,7 +42,7 @@ func main() { registerMiddlewareTypes() // 3. Run server from config folder - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { panic(err) } } diff --git a/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/main.go b/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/main.go index 6f08779c..017f5b1e 100644 --- a/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/main.go +++ b/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/main.go @@ -1,8 +1,8 @@ package main import ( - "github.com/primadi/lokstra" "github.com/primadi/lokstra/common/logger" + "github.com/primadi/lokstra/lokstra_registry" ) func main() { @@ -23,11 +23,11 @@ func main() { registerMiddlewareTypes() // 3. RunServerFromConfig - if err := lokstra.LoadConfig(); err != nil { + if err := lokstra_registry.LoadConfig(); err != nil { logger.LogPanic("❌ Failed to load config:", err) } - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { logger.LogPanic("❌ Failed to run server:", err) } } diff --git a/docs/00-introduction/examples/full-framework/04-external-services/main.go b/docs/00-introduction/examples/full-framework/04-external-services/main.go index ee653d67..be71d71d 100644 --- a/docs/00-introduction/examples/full-framework/04-external-services/main.go +++ b/docs/00-introduction/examples/full-framework/04-external-services/main.go @@ -3,7 +3,6 @@ package main import ( "strings" - "github.com/primadi/lokstra" "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/deploy" svc "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/04-external-services/service" @@ -34,11 +33,11 @@ func main() { printStartInfo() - if err := lokstra.LoadConfig(); err != nil { + if err := lokstra_registry.LoadConfig(); err != nil { logger.LogPanic("❌ Failed to load config:", err) } - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { logger.LogPanic("❌ Failed to run server:", err) } diff --git a/docs/00-introduction/examples/full-framework/05-remote-router/main.go b/docs/00-introduction/examples/full-framework/05-remote-router/main.go index 3e1a4b7e..4862f366 100644 --- a/docs/00-introduction/examples/full-framework/05-remote-router/main.go +++ b/docs/00-introduction/examples/full-framework/05-remote-router/main.go @@ -22,7 +22,7 @@ func main() { svc.WeatherServiceFactory) // Load config and build deployment topology - if err := loader.LoadAndBuild([]string{"config.yaml"}); err != nil { + if _, err := loader.LoadConfig("config.yaml"); err != nil { log.Fatalf("Failed to load config: %v", err) } diff --git a/docs/00-introduction/examples/full-framework/06-inline-definitions-example/main.go b/docs/00-introduction/examples/full-framework/06-inline-definitions-example/main.go index 41e21b1e..128257dc 100644 --- a/docs/00-introduction/examples/full-framework/06-inline-definitions-example/main.go +++ b/docs/00-introduction/examples/full-framework/06-inline-definitions-example/main.go @@ -1,8 +1,8 @@ package main import ( - "github.com/primadi/lokstra" "github.com/primadi/lokstra/common/logger" + "github.com/primadi/lokstra/lokstra_registry" ) func main() { @@ -12,11 +12,11 @@ func main() { // Get config path // configPath := filepath.Join("docs", "00-introduction", "examples", "full-framework", "06-inline-definitions-example", "config.yaml") - if err := lokstra.LoadConfig(); err != nil { + if err := lokstra_registry.LoadConfig(); err != nil { logger.LogPanic("❌ Failed to load config:", err) } - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { logger.LogPanic("❌ Failed to run server:", err) } diff --git a/docs/02-framework-guide/04-config/examples/01-basic-config/README.md b/docs/02-framework-guide/04-config/examples/01-basic-config/README.md index 564da859..ab8b15f5 100644 --- a/docs/02-framework-guide/04-config/examples/01-basic-config/README.md +++ b/docs/02-framework-guide/04-config/examples/01-basic-config/README.md @@ -89,7 +89,7 @@ func main() { lokstra.Bootstrap() // STEP 2: Load Config - loads YAML configuration - lokstra.LoadConfig("config.yaml") + lokstra_registry.LoadConfig("config.yaml") // STEP 3: Register Service Types - map factory names to functions registerServiceTypes() diff --git a/docs/02-framework-guide/04-config/examples/01-basic-config/main.go b/docs/02-framework-guide/04-config/examples/01-basic-config/main.go index 333fe952..9adc6504 100644 --- a/docs/02-framework-guide/04-config/examples/01-basic-config/main.go +++ b/docs/02-framework-guide/04-config/examples/01-basic-config/main.go @@ -3,17 +3,17 @@ package main import ( "log" - "github.com/primadi/lokstra" + "github.com/primadi/lokstra/lokstra_init" "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/middleware/recovery" ) func main() { // Auto-generate code from @RouterService annotations - lokstra.Bootstrap() + lokstra_init.Bootstrap() // STEP 1: Load Config - if err := lokstra.LoadConfig("config.yaml"); err != nil { + if err := lokstra_registry.LoadConfig("config.yaml"); err != nil { log.Fatal("Failed to load config:", err) } @@ -24,7 +24,7 @@ func main() { registerMiddlewareTypes() // STEP 4: Initialize and Run Server - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { log.Fatal("Failed to run server:", err) } } diff --git a/docs/02-framework-guide/04-config/examples/02-multi-file/main.go b/docs/02-framework-guide/04-config/examples/02-multi-file/main.go index b8e3e161..476f0060 100644 --- a/docs/02-framework-guide/04-config/examples/02-multi-file/main.go +++ b/docs/02-framework-guide/04-config/examples/02-multi-file/main.go @@ -1,21 +1,22 @@ package main import ( - "github.com/primadi/lokstra" "github.com/primadi/lokstra/common/logger" + "github.com/primadi/lokstra/lokstra_init" + "github.com/primadi/lokstra/lokstra_registry" ) func main() { - lokstra.Bootstrap() + lokstra_init.Bootstrap() - if err := lokstra.LoadConfig( + if err := lokstra_registry.LoadConfig( "config/base.yaml", "config/dev.yaml", // or production.yaml for prod ); err != nil { logger.LogPanic("❌ Failed to load config:", err) } - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { logger.LogPanic("❌ Failed to run server:", err) } diff --git a/docs/02-framework-guide/04-config/examples/06-handlers/main.go b/docs/02-framework-guide/04-config/examples/06-handlers/main.go index ccf5d45c..f9abfc85 100644 --- a/docs/02-framework-guide/04-config/examples/06-handlers/main.go +++ b/docs/02-framework-guide/04-config/examples/06-handlers/main.go @@ -3,16 +3,16 @@ package main import ( "log" - "github.com/primadi/lokstra" + "github.com/primadi/lokstra/lokstra_init" "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/middleware/recovery" ) func main() { - lokstra.Bootstrap() + lokstra_init.Bootstrap() // STEP 1: Load Config - if err := lokstra.LoadConfig("config.yaml"); err != nil { + if err := lokstra_registry.LoadConfig("config.yaml"); err != nil { log.Fatal("Failed to load config:", err) } @@ -23,7 +23,7 @@ func main() { registerMiddlewareTypes() // STEP 4: Initialize and Run Server - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { log.Fatal("Failed to run server:", err) } } diff --git a/docs/02-framework-guide/08-database-pools.md b/docs/02-framework-guide/08-database-pools.md index 5b90aaea..11080b4f 100644 --- a/docs/02-framework-guide/08-database-pools.md +++ b/docs/02-framework-guide/08-database-pools.md @@ -43,7 +43,7 @@ func main() { lokstra.Bootstrap() // 1. Load config only - if err := lokstra.LoadConfig("config.yaml"); err != nil { + if err := lokstra_registry.LoadConfig("config.yaml"); err != nil { log.Fatal(err) } @@ -151,7 +151,7 @@ named-db-pools: ✅ **Good:** ```go // Load config first, setup DB later -lokstra.LoadConfig("config.yaml") +lokstra_registry.LoadConfig("config.yaml") lokstra.SetupNamedDbPools() ``` @@ -209,7 +209,7 @@ export DB_SSLMODE=require ```go func TestUserService(t *testing.T) { // Load config without setting up DB pools - lokstra.LoadConfig("config.yaml") + lokstra_registry.LoadConfig("config.yaml") // Mock DB pool mockDB := &MockDbPool{} diff --git a/docs/BOOTSTRAP-FLOWS.md b/docs/BOOTSTRAP-FLOWS.md deleted file mode 100644 index 82824538..00000000 --- a/docs/BOOTSTRAP-FLOWS.md +++ /dev/null @@ -1,441 +0,0 @@ -# Lokstra Bootstrap Flows - -This document explains the two approaches for bootstrapping a Lokstra application. - -## Table of Contents - -1. [Flow Comparison](#flow-comparison) -2. [Old Flow (Current)](#old-flow-current) -3. [New Flow (Recommended)](#new-flow-recommended) -4. [Migration Guide](#migration-guide) -5. [Examples](#examples) - ---- - -## Flow Comparison - -### Old Flow (Current) - -``` -1. RegisterServiceTypes() // Using code only -2. RegisterMiddlewareTypes() // Using code only -3. RunServerFromConfig() // Load config + start server - └─ LoadAndBuild() // Register lazy services from YAML - └─ RunServer() // Start server -``` - -**Problems:** -- ❌ Config not available during service/middleware registration -- ❌ Services that need config must use lazy loading -- ❌ Config validation happens late (at server start) - -### New Flow (Recommended) - -``` -1. LoadConfig() // Load YAML config - └─ LoadAndBuild() // Register lazy services from YAML -2. RegisterServiceTypes() // Config is available here! -3. RegisterMiddlewareTypes() // Config is available here! -4. InitAndRunServer() // Start server only -``` - -**Benefits:** -- ✅ Config available during service/middleware registration -- ✅ Early config validation -- ✅ Services can read config in factories without lazy loading -- ✅ More intuitive and easier to debug - ---- - -## Old Flow (Current) - -### Code Pattern - -```go -package main - -import ( - "github.com/primadi/lokstra" - "github.com/primadi/lokstra/core/deploy" - "github.com/primadi/lokstra/lokstra_registry" -) - -func main() { - lokstra.Bootstrap() - deploy.SetLogLevelFromEnv() - - // 1. Register service types (no config available) - registerServiceTypes() - - // 2. Register middleware types (no config available) - registerMiddlewareTypes() - - // 3. Load config + run server - lokstra_registry.RunServerFromConfigFolder("config") -} -``` - -### Service Factory (Old Flow) - -```go -// Problem: Can't access config during registration -func UserServiceFactory(deps map[string]any, config map[string]any) any { - // config parameter only contains service-level config from YAML - // Cannot access global config like database.dsn - - return &UserServiceImpl{ - UserRepo: service.Cast[UserRepository](deps["user-repository"]), - } -} -``` - ---- - -## New Flow (Recommended) - -### Code Pattern - -```go -package main - -import ( - "log" - "github.com/primadi/lokstra" - "github.com/primadi/lokstra/core/deploy" -) - -func main() { - lokstra.Bootstrap() - deploy.SetLogLevelFromEnv() - - // 1. Load config FIRST - if err := lokstra.LoadConfigFromFolder("config"); err != nil { - log.Fatal("Failed to load config:", err) - } - - // 2. Register service types (config IS available now!) - registerServiceTypes() - - // 3. Register middleware types (config IS available now!) - registerMiddlewareTypes() - - // 4. Initialize and run server - if err := lokstra.InitAndRunServer(); err != nil { - log.Fatal("Failed to run server:", err) - } -} -``` - -### Service Factory (New Flow) - -```go -import "github.com/primadi/lokstra/lokstra_registry" - -// Solution: Access global config during registration -func UserServiceFactory(deps map[string]any, config map[string]any) any { - // Now you can access global config! - dbDSN := lokstra_registry.GetConfig("database.dsn", "postgres://localhost/mydb") - cacheEnabled := lokstra_registry.GetConfig("cache.enabled", true) - - log.Printf("Creating UserService with DSN: %s, Cache: %v", dbDSN, cacheEnabled) - - return &UserServiceImpl{ - UserRepo: service.Cast[UserRepository](deps["user-repository"]), - CacheEnabled: cacheEnabled, - } -} -``` - ---- - -## Migration Guide - -### Step 1: Update main.go - -**Before (Old Flow):** -```go -func main() { - lokstra.Bootstrap() - deploy.SetLogLevelFromEnv() - - registerServiceTypes() - registerMiddlewareTypes() - - lokstra_registry.RunServerFromConfigFolder("config") -} -``` - -**After (New Flow):** -```go -func main() { - lokstra.Bootstrap() - deploy.SetLogLevelFromEnv() - - // Load config first - if err := lokstra.LoadConfigFromFolder("config"); err != nil { - log.Fatal("Failed to load config:", err) - } - - // Register services (config available) - registerServiceTypes() - registerMiddlewareTypes() - - // Start server - if err := lokstra.InitAndRunServer(); err != nil { - log.Fatal("Failed to run server:", err) - } -} -``` - -### Step 2: Update Service Factories (Optional) - -You can now access global config in your factories: - -```go -func MyServiceFactory(deps map[string]any, config map[string]any) any { - // Access global config from YAML - apiKey := lokstra_registry.GetConfig("external.api_key", "") - timeout := lokstra_registry.GetConfig("external.timeout", 30) - - return &MyServiceImpl{ - APIKey: apiKey, - Timeout: time.Duration(timeout) * time.Second, - } -} -``` - -### Step 3: Add Global Config to YAML (Optional) - -You can now use a global configs section: - -```yaml -# config/app.yaml -configs: - database: - dsn: "postgres://localhost:5432/mydb" - cache: - enabled: true - ttl: 300 - external: - api_key: "${API_KEY}" - timeout: 30 - -service-definitions: - user-service: - type: user-service-factory - # Service-level config still works - config: - some_service_specific_setting: "value" -``` - ---- - -## Examples - -### Example 1: Simple App - -```go -package main - -import ( - "log" - "github.com/primadi/lokstra" -) - -func main() { - lokstra.Bootstrap() - - // Load single config file - if err := lokstra.LoadConfig("config.yaml"); err != nil { - log.Fatal(err) - } - - registerServiceTypes() - - if err := lokstra.InitAndRunServer(); err != nil { - log.Fatal(err) - } -} -``` - -### Example 2: Multiple Config Files - -```go -package main - -import ( - "log" - "github.com/primadi/lokstra" -) - -func main() { - lokstra.Bootstrap() - - // Load multiple config files - if err := lokstra.LoadConfig( - "config/base.yaml", - "config/services.yaml", - "config/deployments.yaml", - ); err != nil { - log.Fatal(err) - } - - registerServiceTypes() - registerMiddlewareTypes() - - if err := lokstra.InitAndRunServer(); err != nil { - log.Fatal(err) - } -} -``` - -### Example 3: Config Folder - -```go -package main - -import ( - "log" - "github.com/primadi/lokstra" -) - -func main() { - lokstra.Bootstrap() - - // Load all YAML files from folder - if err := lokstra.LoadConfigFromFolder("config"); err != nil { - log.Fatal(err) - } - - registerServiceTypes() - registerMiddlewareTypes() - - if err := lokstra.InitAndRunServer(); err != nil { - log.Fatal(err) - } -} -``` - -### Example 4: Accessing Config in Service Factory - -```go -import "github.com/primadi/lokstra/lokstra_registry" - -func DatabaseServiceFactory(deps map[string]any, config map[string]any) any { - // Get from global config - dsn := lokstra_registry.GetConfig("database.dsn", "postgres://localhost/mydb") - maxConns := lokstra_registry.GetConfig("database.max_connections", 25) - - // Get from service-level config (still works!) - poolSize := 10 - if ps, ok := config["pool_size"].(int); ok { - poolSize = ps - } - - log.Printf("Creating database service: DSN=%s, MaxConns=%d, PoolSize=%d", - dsn, maxConns, poolSize) - - return &DatabaseService{ - DSN: dsn, - MaxConnections: maxConns, - PoolSize: poolSize, - } -} -``` - ---- - -## API Reference - -### New Functions - -#### `LoadConfig(configPaths ...string) error` - -Loads YAML configuration file(s) and registers lazy load services. - -```go -// Single file -err := lokstra.LoadConfig("config.yaml") - -// Multiple files -err := lokstra.LoadConfig( - "config/base.yaml", - "config/services.yaml", -) -``` - -#### `LoadConfigFromFolder(configFolder string) error` - -Loads all YAML files from the specified folder. - -```go -err := lokstra.LoadConfigFromFolder("config") -``` - -#### `InitAndRunServer() error` - -Initializes and runs the server based on loaded config. - -```go -// Reads these config keys: -// - server: Server selection (optional, uses first if not specified) -// - shutdown_timeout: Graceful shutdown timeout (optional, default: 30s) - -err := lokstra.InitAndRunServer() -``` - -#### `GetConfig[T any](key string, defaultValue T) T` - -Retrieves a configuration value with type safety. - -```go -// String -dsn := lokstra_registry.GetConfig("database.dsn", "postgres://localhost/mydb") - -// Int -maxConns := lokstra_registry.GetConfig("database.max_connections", 25) - -// Bool -cacheEnabled := lokstra_registry.GetConfig("cache.enabled", true) - -// Duration (as int seconds) -timeoutSec := lokstra_registry.GetConfig("timeout", 30) -timeout := time.Duration(timeoutSec) * time.Second -``` - ---- - -## FAQ - -### Q: Do I need to migrate to the new flow? - -**A:** No, the old flow still works. However, the new flow is recommended for new projects and provides better developer experience. - -### Q: Can I use both flows in the same project? - -**A:** No, choose one flow per project. They are mutually exclusive. - -### Q: What if I don't need to access config in my services? - -**A:** The new flow is still recommended for better organization and early config validation. - -### Q: Does this change how YAML config works? - -**A:** No, YAML structure remains the same. You can now optionally add a `configs` section for global values. - -### Q: What about environment variables? - -**A:** Environment variable substitution (e.g., `${DATABASE_URL}`) still works in both flows. - ---- - -## Conclusion - -The **new flow** provides better separation of concerns and makes config available during service registration. It's the recommended approach for all new projects. - -**Key Takeaway:** - -``` -Old: Register → Load Config → Run -New: Load Config → Register → Run ✅ -``` diff --git a/docs/CONFIG-FIRST-IMPLEMENTATION.md b/docs/CONFIG-FIRST-IMPLEMENTATION.md deleted file mode 100644 index aff5a630..00000000 --- a/docs/CONFIG-FIRST-IMPLEMENTATION.md +++ /dev/null @@ -1,266 +0,0 @@ -# Lokstra Config-First Bootstrap Flow - Implementation Summary - -## Masalah yang Dipecahkan - -### Pendekatan Lama -``` -1. RegisterServiceTypes() ❌ Config belum tersedia -2. RegisterMiddlewareTypes() ❌ Config belum tersedia -3. RunServerFromConfigFolder() ✅ Config baru di-load disini -``` - -**Problems:** -- Service factories tidak bisa akses config saat registration -- Harus menggunakan lazy loading untuk semua service yang butuh config -- Config validation terlambat (saat server start) - -### Pendekatan Baru (Recommended) -``` -1. LoadConfigFromFolder() ✅ Config di-load lebih awal -2. RegisterServiceTypes() ✅ Config sudah tersedia! -3. RegisterMiddlewareTypes() ✅ Config sudah tersedia! -4. InitAndRunServer() ✅ Hanya start server -``` - -**Benefits:** -- ✅ Config tersedia saat service/middleware registration -- ✅ Early validation (error config terdeteksi lebih awal) -- ✅ Service factories bisa baca global config -- ✅ Lebih intuitif dan mudah di-debug - -## Files Changed - -### 1. `lokstra_registry/helper.go` -Menambahkan fungsi-fungsi baru: - -- **`LoadConfig(configPaths ...string) error`** - - Load YAML config file(s) - - Makes config available for subsequent registration - -- **`LoadConfigFromFolder(configFolder string) error`** - - Load all YAML files from folder - - Convenience wrapper around LoadConfig - -- **`InitAndRunServer() error`** - - Initialize and run server from loaded config - - Reads server selection and shutdown timeout from config - - Must be called after LoadConfig and registration - -### 2. Documentation Files Created - -- **`docs/BOOTSTRAP-FLOWS.md`** - - Complete documentation of both flows - - Migration guide - - Examples - - API reference - - FAQ - -- **`project_templates/.../main_new_flow.go`** - - Example implementation using new flow - - Side-by-side with old flow for comparison - -- **`project_templates/.../README_FLOWS.md`** - - Quick reference for template users - -### 3. Updated AI Documentation - -- **`.github/copilot-instructions.md`** - - Updated with new recommended flow - - Shows both approaches - - Includes config access pattern - -## API Usage - -### New Flow (Recommended) - -```go -package main - -import ( - "log" - "github.com/primadi/lokstra" - "github.com/primadi/lokstra/core/deploy" -) - -func main() { - lokstra.Bootstrap() - deploy.SetLogLevelFromEnv() - - // 1. Load config FIRST - if err := lokstra.LoadConfigFromFolder("config"); err != nil { - log.Fatal("Failed to load config:", err) - } - - // 2. Register services (config is available now!) - registerServiceTypes() - - // 3. Register middleware (config is available now!) - registerMiddlewareTypes() - - // 4. Initialize and run server - if err := lokstra.InitAndRunServer(); err != nil { - log.Fatal("Failed to run server:", err) - } -} -``` - -### Service Factory dengan Config Access - -```go -import "github.com/primadi/lokstra/lokstra_registry" - -func UserServiceFactory(deps map[string]any, config map[string]any) any { - // Access global config from YAML! - dbDSN := lokstra_registry.GetConfig("database.dsn", "postgres://localhost/mydb") - cacheEnabled := lokstra_registry.GetConfig("cache.enabled", true) - cacheTTL := lokstra_registry.GetConfig("cache.ttl", 300) - - log.Printf("Creating UserService: DSN=%s, Cache=%v, TTL=%d", - dbDSN, cacheEnabled, cacheTTL) - - return &UserServiceImpl{ - UserRepo: service.Cast[UserRepository](deps["user-repository"]), - CacheEnabled: cacheEnabled, - CacheTTL: time.Duration(cacheTTL) * time.Second, - } -} -``` - -### Config YAML dengan Global Configs - -```yaml -# Global configs (optional, new feature!) -configs: - database: - dsn: "postgres://localhost:5432/mydb" - cache: - enabled: true - ttl: 300 - external: - api_key: "${API_KEY}" - timeout: 30 - -service-definitions: - user-service: - type: user-service-factory - depends-on: [user-repository] - # Service-level config still works - config: - some_service_specific: "value" -``` - -## Backward Compatibility - -**Old flow masih tetap didukung:** - -```go -func main() { - lokstra.Bootstrap() - deploy.SetLogLevelFromEnv() - - registerServiceTypes() - registerMiddlewareTypes() - - // Old way - still works! - lokstra_registry.RunServerFromConfigFolder("config") -} -``` - -Tidak ada breaking changes! Projects existing bisa tetap menggunakan old flow. - -## Migration Guide - -### Step 1: Update main.go - -Replace `RunServerFromConfigFolder()` with 3 separate calls: - -```go -// Before -lokstra_registry.RunServerFromConfigFolder("config") - -// After -if err := lokstra.LoadConfigFromFolder("config"); err != nil { - log.Fatal(err) -} -// ... register services/middlewares ... -if err := lokstra.InitAndRunServer(); err != nil { - log.Fatal(err) -} -``` - -### Step 2: Access Config in Factories (Optional) - -```go -func MyServiceFactory(deps map[string]any, config map[string]any) any { - // NEW: Access global config - apiKey := lokstra_registry.GetConfig("external.api_key", "") - timeout := lokstra_registry.GetConfig("external.timeout", 30) - - // Use the values - return &MyService{ - APIKey: apiKey, - Timeout: time.Duration(timeout) * time.Second, - } -} -``` - -### Step 3: Add Global Config Section (Optional) - -```yaml -# Add to your config YAML -configs: - external: - api_key: "${API_KEY}" - timeout: 30 - database: - dsn: "${DATABASE_URL}" -``` - -## Testing - -All new functions have been tested: - -```bash -# Check for syntax errors -go vet ./lokstra_registry - -# Result: No errors (quic-go errors are from dependencies, not our code) -``` - -## Next Steps - -1. ✅ **Implementation Complete** - - New API functions added - - Documentation created - - Examples provided - -2. 📚 **Documentation** - - Update main docs site with new flow - - Add to getting started guide - - Update tutorials - -3. 🎯 **Future Enhancements** - - Add config validation schema - - Add config watcher for hot reload - - Add config encryption support - -## Summary - -Implementasi baru ini memberikan: - -1. **Better Developer Experience** - - Config available saat registration - - Early error detection - - More intuitive flow - -2. **Backward Compatible** - - Old flow tetap berfungsi - - No breaking changes - - Gradual migration possible - -3. **More Flexible** - - Services dapat access global config - - Config validation lebih awal - - Easier testing and debugging - -**Recommended for all new projects!** 🚀 diff --git a/lokstra_helper.go b/lokstra_helper.go index 4648600f..9c3971d8 100644 --- a/lokstra_helper.go +++ b/lokstra_helper.go @@ -1,7 +1,6 @@ package lokstra import ( - "github.com/primadi/lokstra/api_client" "github.com/primadi/lokstra/core/app" "github.com/primadi/lokstra/core/request" "github.com/primadi/lokstra/core/router" @@ -38,9 +37,3 @@ func NewAppWithConfig(name string, addr string, listenerType string, func NewServer(name string, apps ...*app.App) *server.Server { return server.New(name, apps...) } - -// FetchAndCast performs an API fetch and casts the result to the specified type T -func FetchAndCast[T any](client *api_client.ClientRouter, path string, - opts ...api_client.FetchOption) (T, error) { - return api_client.FetchAndCast[T](client, path, opts...) -} diff --git a/lokstra_bootstrap.go b/lokstra_init/bootstrap.go similarity index 87% rename from lokstra_bootstrap.go rename to lokstra_init/bootstrap.go index fa2f6402..fca5b361 100644 --- a/lokstra_bootstrap.go +++ b/lokstra_init/bootstrap.go @@ -1,4 +1,4 @@ -package lokstra +package lokstra_init import ( "fmt" @@ -27,8 +27,6 @@ var ( // Bootstrap initializes Lokstra environment and regenerates routes if needed. // It must be called at the very beginning of main(). -// It will auto create dbpool-manager service using PgxPoolManager if not exists. -// // scanPath specifies additional paths to scan for annotations (besides current working directory). // If --generate-only flag is present, it will only run code generation and exit. // Example: @@ -238,25 +236,7 @@ func relaunchWithDlv() { os.Exit(0) } -// LoadConfigFromFolder loads configuration from the specified folder path. -// It also ensures that the dbpool-manager service is registered before loading config. -func LoadConfigFromFolder(folderPath string) error { - return lokstra_registry.LoadConfigFromFolder(folderPath) -} - -// LoadConfig loads configuration from the specified file path. -// It also ensures that the dbpool-manager service is registered before loading config. -func LoadConfig(filePath ...string) error { - return lokstra_registry.LoadConfig(filePath...) -} - -// LoadNamedDbPoolsFromConfig sets up database pools from loaded config. -// Must be called AFTER LoadConfig() if you use named-db-pools in config. -func LoadNamedDbPoolsFromConfig() error { - return lokstra_registry.LoadNamedDbPoolsFromConfig() -} - -// RunConfiguredServer initializes and runs the server based on loaded configuration. -func RunConfiguredServer() error { - return lokstra_registry.RunConfiguredServer() +// return runtime mode: dev, debug, or prod +func GetRuntimeMode() string { + return lokstra_registry.GetRuntimeMode() } diff --git a/lokstra_init/initialize.go b/lokstra_init/initialize.go new file mode 100644 index 00000000..ad1c0c11 --- /dev/null +++ b/lokstra_init/initialize.go @@ -0,0 +1,160 @@ +package lokstra_init + +import ( + "fmt" + "time" + + "github.com/primadi/lokstra/common/logger" + "github.com/primadi/lokstra/lokstra_registry" + "github.com/primadi/lokstra/services/sync_config_pg" +) + +type InitializeConfig struct { + // If true, panic on configuration error + PanicOnConfigError bool + + // 1. SetLogLevel + LogLevel logger.LogLevel + + // 2.Bootstrap + EnableAnnotation bool + AnnotationScanPaths []string + + // 3. LoadConfig + EnableLoadConfig bool + ConfigPath []string + + // 4. EnablePgxSyncMap + // If true, You have to use DbPoolManager also + EnablePgxSyncMap bool + PgxSyncMapDbPoolName string + PgxSyncHeartbeatInterval time.Duration + PgxSyncReconnectInterval time.Duration + + // 5. EnableDbPoolManager + EnableDbPoolManager bool + IsDbPoolAutoSync bool + + // 6. EnableDbMigration + EnableDbMigration bool + MigrationFolder string + SkipMigrationOnProd bool + + // 7. ServerInit Func + ServerInitFunc func() error + + // 8. Init and Run Server + IsRunServer bool +} + +func (c *InitializeConfig) returnError(err error) error { + if c.PanicOnConfigError && err != nil { + panic(err) + } + + return err +} + +func BootstrapAndRun(opts ...InitializeOption) error { + cfg := &InitializeConfig{ + PanicOnConfigError: true, + LogLevel: logger.LogLevelInfo, + EnableAnnotation: true, // Auto-detect @RouterService + EnableDbPoolManager: false, + IsDbPoolAutoSync: false, + EnablePgxSyncMap: false, + SkipMigrationOnProd: true, + PgxSyncMapDbPoolName: "db_main", + PgxSyncHeartbeatInterval: 5 * time.Minute, + PgxSyncReconnectInterval: 5 * time.Second, + EnableDbMigration: false, + MigrationFolder: "migrations", + IsRunServer: true, + } + + // Apply options + for _, opt := range opts { + opt(cfg) + } + + return BootstrapAndRunWithConfig(cfg) +} + +// Initialize lokstra framework with given config +func BootstrapAndRunWithConfig(cfg *InitializeConfig) error { + if !cfg.EnableDbPoolManager { + if cfg.EnablePgxSyncMap { + return cfg.returnError(fmt.Errorf("PgxSyncMap requires DbPoolManager to be enabled")) + } + if cfg.EnableDbMigration { + return cfg.returnError(fmt.Errorf("DB Migration check requires DbPoolManager to be enabled")) + } + } + + if cfg.EnablePgxSyncMap { + if len(cfg.PgxSyncMapDbPoolName) == 0 { + return cfg.returnError(fmt.Errorf("PgxSyncMapDbPoolName must be set when UsePgxSyncMap is true")) + } + } else if cfg.IsDbPoolAutoSync { + return cfg.returnError(fmt.Errorf("IsDbPoolAutoSync requires PgxSyncMap to be enabled")) + } + + // 1. Set log level + logger.SetLogLevel(cfg.LogLevel) + + // 2. Bootstrap + if cfg.EnableAnnotation { + Bootstrap(cfg.AnnotationScanPaths...) + } + + // 3. LoadConfig + if cfg.EnableLoadConfig { + if err := lokstra_registry.LoadConfig(cfg.ConfigPath...); err != nil { + return cfg.returnError(err) + } + } + + // 4. Use Pgx SyncMap (MUST be before DbPoolManager if IsDbPoolAutoSync=true) + if cfg.EnablePgxSyncMap { + if len(cfg.PgxSyncMapDbPoolName) == 0 { + return cfg.returnError(fmt.Errorf( + "PgxSyncMapDbPoolName must be set when UsePgxSyncMap is true")) + } + sync_config_pg.Register(cfg.PgxSyncMapDbPoolName, + cfg.PgxSyncHeartbeatInterval, cfg.PgxSyncReconnectInterval) + } + + // 5. PgxDbPoolManager (can now use sync-config if IsDbPoolAutoSync=true) + if cfg.EnableDbPoolManager { + UsePgxDbPoolManager(cfg.IsDbPoolAutoSync) + + if err := lokstra_registry.LoadNamedDbPoolsFromConfig(); err != nil { + return cfg.returnError(err) + } + } + + // 6. Check DB Migrations + if cfg.EnableDbMigration { + if mode := GetRuntimeMode(); mode != "prod" || !cfg.SkipMigrationOnProd { + if err := CheckDbMigrationsAuto(cfg.MigrationFolder); err != nil { + return cfg.returnError(err) + } + } + } + + // 7. Server Init Func + if cfg.ServerInitFunc != nil { + if err := cfg.ServerInitFunc(); err != nil { + return cfg.returnError(err) + } + } + + // 8. Init and Run Server + if cfg.IsRunServer { + if err := lokstra_registry.RunConfiguredServer(); err != nil { + return cfg.returnError(err) + } + } + + return nil +} diff --git a/lokstra_migration.go b/lokstra_init/migration.go similarity index 68% rename from lokstra_migration.go rename to lokstra_init/migration.go index f85979e7..7677bd5e 100644 --- a/lokstra_migration.go +++ b/lokstra_init/migration.go @@ -1,4 +1,4 @@ -package lokstra +package lokstra_init import ( "context" @@ -14,21 +14,6 @@ import ( "gopkg.in/yaml.v3" ) -// MigrationForce controls when migrations should run -type MigrationForce string - -const ( - // MigrationForceTrue always runs migrations regardless of mode - MigrationForceTrue MigrationForce = "true" - - // MigrationForceFalse never runs migrations - MigrationForceFalse MigrationForce = "false" - - // MigrationForceAuto runs migrations in dev/debug, skips in prod - // This is the recommended setting for development - MigrationForceAuto MigrationForce = "auto" -) - // MigrationYamlConfig represents the migration.yaml file structure // This file is optional and located in the migrations directory type MigrationYamlConfig struct { @@ -39,9 +24,11 @@ type MigrationYamlConfig struct { // Default: "schema_migrations" SchemaTable string `yaml:"schema-table"` - // Force controls migration execution mode - // Values: "auto" (default), "on", "off" - Force string `yaml:"force"` + // Enabled controls whether migrations are enabled + // nil = enabled by default (true) + // true = explicitly enabled + // false = explicitly disabled + Enabled *bool `yaml:"enabled"` // Description for documentation purposes Description string `yaml:"description"` @@ -62,20 +49,6 @@ type MigrationConfig struct { // Default: "schema_migrations" // Can be overridden by migration.yaml SchemaTable string - - // Force controls when migrations run: - // - "true" or MigrationForceTrue: Always run (even in prod) - // - "false" or MigrationForceFalse: Never run (use CLI instead) - // - "auto" or MigrationForceAuto: Auto-detect based on runtime.mode - // * dev/debug → run migrations - // * prod → skip migrations - // Default: "auto" (recommended for development) - // Can be overridden by migration.yaml - Force MigrationForce - - // Silent suppresses migration output - // Default: false - Silent bool } // CheckDbMigration runs database migrations based on runtime mode @@ -121,7 +94,7 @@ func CheckDbMigration(cfg *MigrationConfig) error { // Try to load migration.yaml from migrations directory yamlPath := filepath.Join(cfg.MigrationsDir, "migration.yaml") - if yamlCfg, err := loadMigrationYaml(yamlPath); err == nil { + if yamlCfg, err := LoadMigrationYaml(yamlPath); err == nil { // Merge YAML config with provided config (YAML takes precedence if not set) if cfg.DbPoolName == "" && yamlCfg.DbPoolName != "" { cfg.DbPoolName = yamlCfg.DbPoolName @@ -129,19 +102,6 @@ func CheckDbMigration(cfg *MigrationConfig) error { if cfg.SchemaTable == "" && yamlCfg.SchemaTable != "" { cfg.SchemaTable = yamlCfg.SchemaTable } - if cfg.Force == "" && yamlCfg.Force != "" { - // Convert YAML force values: "on" -> "true", "off" -> "false" - switch yamlCfg.Force { - case "on": - cfg.Force = MigrationForceTrue - case "off": - cfg.Force = MigrationForceFalse - case "auto": - cfg.Force = MigrationForceAuto - default: - cfg.Force = MigrationForce(yamlCfg.Force) - } - } } // Apply final defaults if still empty @@ -151,34 +111,6 @@ func CheckDbMigration(cfg *MigrationConfig) error { if cfg.SchemaTable == "" { cfg.SchemaTable = "schema_migrations" } - if cfg.Force == "" { - cfg.Force = MigrationForceAuto // Default to auto - } - - // Get current runtime mode - mode := lokstra_registry.GetConfig("runtime.mode", "prod") - - // Determine if migrations should run - shouldRun := false - switch cfg.Force { - case MigrationForceTrue: - shouldRun = true - case MigrationForceFalse: - shouldRun = false - case MigrationForceAuto, "": - // Auto mode: run in dev/debug, skip in prod - shouldRun = (mode == "dev" || mode == "debug") - default: - return fmt.Errorf("invalid Force value: %s (must be 'true', 'false', or 'auto')", cfg.Force) - } - - // Skip if should not run - if !shouldRun { - if !cfg.Silent { - logger.LogInfo("[Lokstra] Skipping migrations (mode=%s, force=%s)", mode, cfg.Force) - } - return nil - } // Get database pool pool, ok := lokstra_registry.GetServiceAny(cfg.DbPoolName) @@ -199,25 +131,22 @@ func CheckDbMigration(cfg *MigrationConfig) error { // Run migrations ctx := context.Background() - if !cfg.Silent { - logger.LogInfo("[Lokstra] Running migrations (mode=%s, force=%s, dir=%s, db=%s, schema=%s)", - mode, cfg.Force, cfg.MigrationsDir, cfg.DbPoolName, cfg.SchemaTable) - } + + logger.LogInfo("[Lokstra] Running migrations (dir=%s, db=%s, schema=%s)", + cfg.MigrationsDir, cfg.DbPoolName, cfg.SchemaTable) if err := runner.Up(ctx); err != nil { return fmt.Errorf("migration failed: %w", err) } - if !cfg.Silent { - logger.LogInfo("[Lokstra] Migrations completed successfully") - } + logger.LogInfo("[Lokstra] Migrations completed successfully") return nil } -// loadMigrationYaml loads and parses migration.yaml file +// LoadMigrationYaml loads and parses migration.yaml file // Returns error if file doesn't exist or cannot be parsed -func loadMigrationYaml(path string) (*MigrationYamlConfig, error) { +func LoadMigrationYaml(path string) (*MigrationYamlConfig, error) { data, err := os.ReadFile(path) if err != nil { return nil, err // File doesn't exist or cannot be read @@ -305,53 +234,47 @@ func CheckDbMigrationsAuto(configFolder string) error { // Run migrations for each folder in order successCount := 0 + errorCount := 0 skippedCount := 0 - mode := lokstra_registry.GetConfig("runtime.mode", "prod") for _, folder := range migrationFolders { folderPath := filepath.Join(rootDir, folder) logger.LogInfo("[Lokstra] Processing migration folder: %s", folder) + // Check if migration is enabled (default: true if not specified) + yamlPath := filepath.Join(folderPath, "migration.yaml") + yamlCfg, _ := LoadMigrationYaml(yamlPath) + + // Default enabled to true if not explicitly set to false + enabled := true + if yamlCfg != nil && yamlCfg.Enabled != nil { + enabled = *yamlCfg.Enabled + } + + if !enabled { + logger.LogInfo("[Lokstra] Skipping disabled migration folder: %s", folder) + skippedCount++ + continue + } + // Run migration for this folder err := CheckDbMigration(&MigrationConfig{ MigrationsDir: folderPath, + DbPoolName: yamlCfg.DbPoolName, + SchemaTable: yamlCfg.SchemaTable, }) if err != nil { - return fmt.Errorf("migration failed for '%s': %w", folder, err) + errorCount++ + logger.LogError("[Lokstra] Migration failed for '%s': %v", folder, err) + // Continue with other folders instead of returning error + continue } - // Count success/skipped based on yaml config - yamlPath := filepath.Join(folderPath, "migration.yaml") - if yamlCfg, _ := loadMigrationYaml(yamlPath); yamlCfg != nil { - shouldRun := false - - switch yamlCfg.Force { - case "on": - shouldRun = true - case "off": - shouldRun = false - case "auto", "": - shouldRun = (mode == "dev" || mode == "debug") - } - - if shouldRun { - successCount++ - } else { - skippedCount++ - } - } else { - // If no yaml config, assume auto mode - shouldRun := (mode == "dev" || mode == "debug") - if shouldRun { - successCount++ - } else { - skippedCount++ - } - } + successCount++ } - logger.LogInfo("[Lokstra] Multi-database migrations completed: %d successful, %d skipped", successCount, skippedCount) + logger.LogInfo("[Lokstra] Multi-database migrations completed: %d successful, %d errors, %d skipped", successCount, errorCount, skippedCount) return nil } diff --git a/migration_test.go b/lokstra_init/migration_test.go similarity index 80% rename from migration_test.go rename to lokstra_init/migration_test.go index 10543ac3..8d6a4acf 100644 --- a/migration_test.go +++ b/lokstra_init/migration_test.go @@ -1,9 +1,11 @@ -package lokstra +package lokstra_init_test import ( "os" "path/filepath" "testing" + + "github.com/primadi/lokstra/lokstra_init" ) func TestLoadMigrationYaml(t *testing.T) { @@ -13,7 +15,7 @@ func TestLoadMigrationYaml(t *testing.T) { // Test 1: File doesn't exist t.Run("file_not_exists", func(t *testing.T) { - _, err := loadMigrationYaml(filepath.Join(tmpDir, "nonexistent.yaml")) + _, err := lokstra_init.LoadMigrationYaml(filepath.Join(tmpDir, "nonexistent.yaml")) if err == nil { t.Error("Expected error for non-existent file, got nil") } @@ -33,7 +35,7 @@ depends-on: t.Fatalf("Failed to write test file: %v", err) } - cfg, err := loadMigrationYaml(yamlPath) + cfg, err := lokstra_init.LoadMigrationYaml(yamlPath) if err != nil { t.Fatalf("Failed to load YAML: %v", err) } @@ -44,9 +46,6 @@ depends-on: if cfg.SchemaTable != "tenant_migrations" { t.Errorf("Expected SchemaTable 'tenant_migrations', got '%s'", cfg.SchemaTable) } - if cfg.Force != "on" { - t.Errorf("Expected Force 'on', got '%s'", cfg.Force) - } if cfg.Description != "Tenant database migrations" { t.Errorf("Expected Description 'Tenant database migrations', got '%s'", cfg.Description) } @@ -59,7 +58,7 @@ depends-on: t.Fatalf("Failed to write test file: %v", err) } - cfg, err := loadMigrationYaml(yamlPath) + cfg, err := lokstra_init.LoadMigrationYaml(yamlPath) if err != nil { t.Fatalf("Failed to load YAML: %v", err) } @@ -80,7 +79,7 @@ depends-on: t.Fatalf("Failed to write test file: %v", err) } - _, err := loadMigrationYaml(yamlPath) + _, err := lokstra_init.LoadMigrationYaml(yamlPath) if err == nil { t.Error("Expected error for invalid YAML, got nil") } @@ -103,7 +102,7 @@ force: off // Test: YAML config should be loaded and merged t.Run("yaml_config_merge", func(t *testing.T) { - cfg := &MigrationConfig{ + cfg := &lokstra_init.MigrationConfig{ // MigrationsDir: tmpDir, // Leave other fields empty - should be filled from YAML } @@ -113,25 +112,13 @@ force: off // by checking the values before it tries to connect // Simulate the merging logic from CheckDbMigration - if yamlCfg, err := loadMigrationYaml(yamlPath); err == nil { + if yamlCfg, err := lokstra_init.LoadMigrationYaml(yamlPath); err == nil { if cfg.DbPoolName == "" && yamlCfg.DbPoolName != "" { cfg.DbPoolName = yamlCfg.DbPoolName } if cfg.SchemaTable == "" && yamlCfg.SchemaTable != "" { cfg.SchemaTable = yamlCfg.SchemaTable } - if cfg.Force == "" && yamlCfg.Force != "" { - switch yamlCfg.Force { - case "on": - cfg.Force = MigrationForceTrue - case "off": - cfg.Force = MigrationForceFalse - case "auto": - cfg.Force = MigrationForceAuto - default: - cfg.Force = MigrationForce(yamlCfg.Force) - } - } } // Apply final defaults @@ -149,21 +136,17 @@ force: off if cfg.SchemaTable != "analytics_migrations" { t.Errorf("Expected SchemaTable 'analytics_migrations' from YAML, got '%s'", cfg.SchemaTable) } - if cfg.Force != MigrationForceFalse { - t.Errorf("Expected Force 'false' from YAML 'off', got '%s'", cfg.Force) - } }) // Test: Explicit config should override YAML t.Run("explicit_config_overrides_yaml", func(t *testing.T) { - cfg := &MigrationConfig{ + cfg := &lokstra_init.MigrationConfig{ // MigrationsDir: tmpDir, DbPoolName: "custom-db", // Explicit value - Force: MigrationForceTrue, } // Simulate merging - explicit values should NOT be overridden - if yamlCfg, err := loadMigrationYaml(yamlPath); err == nil { + if yamlCfg, err := lokstra_init.LoadMigrationYaml(yamlPath); err == nil { if cfg.DbPoolName == "" && yamlCfg.DbPoolName != "" { cfg.DbPoolName = yamlCfg.DbPoolName } @@ -182,10 +165,5 @@ force: off if cfg.DbPoolName != "custom-db" { t.Errorf("Expected explicit DbPoolName 'custom-db', got '%s'", cfg.DbPoolName) } - - // Force should keep explicit value - if cfg.Force != MigrationForceTrue { - t.Errorf("Expected explicit Force 'true', got '%s'", cfg.Force) - } }) } diff --git a/lokstra_init/option.go b/lokstra_init/option.go new file mode 100644 index 00000000..03ca3e8d --- /dev/null +++ b/lokstra_init/option.go @@ -0,0 +1,95 @@ +package lokstra_init + +import ( + "time" + + "github.com/primadi/lokstra/common/logger" +) + +// Options pattern untuk QuickStart +type InitializeOption func(*InitializeConfig) + +// set PanicOnConfigError to true to panic on configuration error +// default is true +func WithPanicOnConfigError(panicOnError bool) InitializeOption { + return func(c *InitializeConfig) { + c.PanicOnConfigError = panicOnError + } +} + +// set log level for lokstra logger +// default is logger.InfoLevel +func WithLogLevel(level logger.LogLevel) InitializeOption { + return func(c *InitializeConfig) { c.LogLevel = level } +} + +// enable annotations with optional scan paths +// if no paths provided, use default paths +// example annotations : @RouterService, @Service, @Route +// default enable is true, path is empty (current folder) +func WithAnnotations(enable bool, paths ...string) InitializeOption { + return func(c *InitializeConfig) { + c.EnableAnnotation = enable + c.AnnotationScanPaths = paths + } +} + +// enable loading configuration from YAML files at the given paths +// paths can be files or folders +func WithYAMLConfigPath(enable bool, paths ...string) InitializeOption { + return func(c *InitializeConfig) { + c.EnableLoadConfig = enable + c.ConfigPath = paths + } +} + +// enable PgxSyncMap with the given db pool name +// default enable is false +func WithPgSyncMap(enable bool, dbPoolName string) InitializeOption { + return func(c *InitializeConfig) { + c.EnablePgxSyncMap = enable + c.PgxSyncMapDbPoolName = dbPoolName + } +} + +// set PgxSyncMap heartbeat and reconnect intervals +// default heartbeat is 5 minutes, reconnect is 5 seconds +func WithPgxSyncMapIntervals(heartBeatInterval, reconnectInterval time.Duration) InitializeOption { + return func(c *InitializeConfig) { + c.PgxSyncHeartbeatInterval = heartBeatInterval + c.PgxSyncReconnectInterval = reconnectInterval + } +} + +// enable or disable database pool manager +// default enable is false +func WithDbPoolManager(enable bool, isDbPoolAutoSync bool) InitializeOption { + return func(c *InitializeConfig) { + c.EnableDbPoolManager = enable + c.IsDbPoolAutoSync = isDbPoolAutoSync + } +} + +// enable or disable database migrations with the given migration folder +// default enable is false +func WithDbMigrations(enable bool, folder string) InitializeOption { + return func(c *InitializeConfig) { + c.EnableDbMigration = enable + c.MigrationFolder = folder + } +} + +// set server initialization function +func WithServerInitFunc(initFunc func() error) InitializeOption { + return func(c *InitializeConfig) { + c.ServerInitFunc = initFunc + } +} + +// enable or disable automatic running of the server after initialization +// default is true +func WithAutoRunServer(enable bool) InitializeOption { + return func(c *InitializeConfig) { + c.IsRunServer = enable + } +} diff --git a/lokstra_pgx_dbpool_manager.go b/lokstra_init/pgx_dbpool_manager.go similarity index 97% rename from lokstra_pgx_dbpool_manager.go rename to lokstra_init/pgx_dbpool_manager.go index 5e51b626..3a8bd7ee 100644 --- a/lokstra_pgx_dbpool_manager.go +++ b/lokstra_init/pgx_dbpool_manager.go @@ -1,4 +1,4 @@ -package lokstra +package lokstra_init import ( "github.com/primadi/lokstra/common/logger" diff --git a/lokstra_init_option.go b/lokstra_init_option.go deleted file mode 100644 index 1061668c..00000000 --- a/lokstra_init_option.go +++ /dev/null @@ -1,96 +0,0 @@ -package lokstra - -import ( - "github.com/primadi/lokstra/common/logger" -) - -// Options pattern untuk QuickStart -type InitializeOption func(*InitializeConfig) - -func WithLogLevel(level logger.LogLevel) InitializeOption { - return func(c *InitializeConfig) { c.LogLevel = level } -} - -func WithAnnotations(enable bool, paths ...string) InitializeOption { - return func(c *InitializeConfig) { - c.UseAnnotation = enable - c.AnnotationScanPaths = paths - } -} - -func WithoutYAMLConfig() InitializeOption { - return func(c *InitializeConfig) { - c.ConfigSource = ConfigNone - c.ConfigPath = nil - } -} - -func WithYAMLConfigPath(paths ...string) InitializeOption { - return func(c *InitializeConfig) { - c.ConfigSource = ConfigFromFile - c.ConfigPath = paths - } -} - -func WithYAMLConfigFolder(folder string) InitializeOption { - return func(c *InitializeConfig) { - c.ConfigSource = ConfigFromFolder - c.ConfigPath = []string{folder} - } -} - -func WithoutPgSyncMap() InitializeOption { - return func(c *InitializeConfig) { - c.UsePgxSyncMap = false - } -} - -func WithPgSyncMap(dbPoolName ...string) InitializeOption { - return func(c *InitializeConfig) { - c.UsePgxSyncMap = true - if len(dbPoolName) == 0 { - c.PgxSyncMapDbPoolName = "db_main" - } else { - if len(dbPoolName) > 1 { - panic("WithPgSyncMap only accepts zero or one argument for dbPoolName") - } - c.PgxSyncMapDbPoolName = dbPoolName[0] - } - } -} - -func WithoutPgDatabase() InitializeOption { - return func(c *InitializeConfig) { - c.DbPoolManagerEnable = false - } -} - -func WithPgDatabase() InitializeOption { - return func(c *InitializeConfig) { - c.DbPoolManagerEnable = true - } -} - -func WithoutDbMigrations() InitializeOption { - return func(c *InitializeConfig) { - c.CheckDbMigration = false - } -} - -func WithDbMigrations(folder string) InitializeOption { - return func(c *InitializeConfig) { - c.CheckDbMigration = true - c.MigrationFolder = folder - } -} - -func WithServerInitFunc(initFunc func() error) InitializeOption { - return func(c *InitializeConfig) { - c.ServerInitFunc = initFunc - } -} -func WithoutAutoRunServer() InitializeOption { - return func(c *InitializeConfig) { - c.InitAndRunServer = false - } -} diff --git a/lokstra_initialize.go b/lokstra_initialize.go deleted file mode 100644 index 0c240022..00000000 --- a/lokstra_initialize.go +++ /dev/null @@ -1,150 +0,0 @@ -package lokstra - -import ( - "fmt" - - "github.com/primadi/lokstra/common/logger" - "github.com/primadi/lokstra/services/sync_config_pg" -) - -type ConfigSourceType int - -const ( - ConfigNone ConfigSourceType = iota - ConfigFromFile - ConfigFromFolder -) - -type InitializeConfig struct { - // 1. SetLogLevel - LogLevel logger.LogLevel - - // 2.Bootstrap - UseAnnotation bool - AnnotationScanPaths []string - - // 3. LoadConfig - ConfigSource ConfigSourceType - ConfigPath []string - - // 4. UsePgxSyncMap - // If true, You have to use DbPoolManager also - UsePgxSyncMap bool - PgxSyncMapDbPoolName string - - // 5. DbPoolManagerEnable - DbPoolManagerEnable bool - - // 6. CheckDbMigrations - CheckDbMigration bool - MigrationFolder string - - // 7. ServerInit Func - ServerInitFunc func() error - - // 8. Init and Run Server - InitAndRunServer bool -} - -func BootstrapAndRun(opts ...InitializeOption) error { - cfg := &InitializeConfig{ - LogLevel: logger.LogLevelInfo, - UseAnnotation: true, // Auto-detect @RouterService - ConfigSource: ConfigFromFolder, - DbPoolManagerEnable: true, // Auto if config has database.* - UsePgxSyncMap: true, - PgxSyncMapDbPoolName: "db_main", - CheckDbMigration: true, - MigrationFolder: "migrations", - InitAndRunServer: true, - } - - // Apply options - for _, opt := range opts { - opt(cfg) - } - - return BootstrapAndRunWithConfig(cfg) -} - -// Initialize lokstra framework with given config -func BootstrapAndRunWithConfig(cfg *InitializeConfig) error { - if !cfg.DbPoolManagerEnable { - if cfg.UsePgxSyncMap { - return fmt.Errorf("PgxSyncMap requires DbPoolManager to be enabled") - } - if cfg.CheckDbMigration { - return fmt.Errorf("DB Migration check requires DbPoolManager to be enabled") - } - } - - // 1. Set log level - logger.SetLogLevel(cfg.LogLevel) - - // 2. Bootstrap - if cfg.UseAnnotation { - Bootstrap(cfg.AnnotationScanPaths...) - } - - // 3. LoadConfig - switch cfg.ConfigSource { - case ConfigFromFile: - if len(cfg.ConfigPath) == 0 { - cfg.ConfigPath = []string{"config.yaml"} - } - if err := LoadConfig(cfg.ConfigPath...); err != nil { - return err - } - case ConfigFromFolder: - lenConfig := len(cfg.ConfigPath) - if lenConfig == 0 { - cfg.ConfigPath = []string{"config"} - } else if lenConfig > 1 { - return fmt.Errorf("ConfigPath for ConfigFromFolder should contain only one folder path") - } - - if err := LoadConfigFromFolder(cfg.ConfigPath[0]); err != nil { - return err - } - } - - // 4. Use Pgx SyncMap - if cfg.UsePgxSyncMap { - if len(cfg.PgxSyncMapDbPoolName) == 0 { - return fmt.Errorf("PgxSyncMapDbPoolName must be set when UsePgxSyncMap is true") - } - sync_config_pg.Register(cfg.PgxSyncMapDbPoolName) - } - - // 5. PgxDbPoolManager - if cfg.DbPoolManagerEnable { - UsePgxDbPoolManager(cfg.UsePgxSyncMap) - - if err := LoadNamedDbPoolsFromConfig(); err != nil { - return err - } - } - - // 6. Check DB Migrations - if cfg.CheckDbMigration { - if err := CheckDbMigrationsAuto(cfg.MigrationFolder); err != nil { - return err - } - } - - // 7. Server Init Func - if cfg.ServerInitFunc != nil { - if err := cfg.ServerInitFunc(); err != nil { - return err - } - } - - // 8. Init and Run Server - if cfg.InitAndRunServer { - if err := RunConfiguredServer(); err != nil { - return err - } - } - - return nil -} diff --git a/lokstra_registry/helper.go b/lokstra_registry/helper.go index ea531f62..3335ef25 100644 --- a/lokstra_registry/helper.go +++ b/lokstra_registry/helper.go @@ -1,35 +1,22 @@ package lokstra_registry import ( - "path/filepath" "time" "github.com/primadi/lokstra/common/logger" - "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/core/deploy/loader" ) -// LoadConfig loads YAML configuration file(s) and registers lazy load services. -// This makes config available for service/middleware registration. -// Config paths are relative to the project base path. -// -// Example: -// -// if err := lokstra_registry.LoadConfig("config.yaml"); err != nil { -// logger.LogPanic(err) -// } -// -// After calling LoadConfig, you can: -// - Access config via GetConfig() -// - Register services/middlewares (they can read config) -// - Call InitAndRunServer() to start the server +// LoadConfig loads configuration from the specified file path. +// path can be one or more YAML files or folders. +// If no path is provided, it defaults to "config" folder func LoadConfig(configPaths ...string) error { if len(configPaths) == 0 { - configPaths = []string{"config.yaml"} + configPaths = []string{"config"} } // Load config (loads ALL deployments into Global registry) - if err := loader.LoadAndBuild(configPaths); err != nil { + if _, err := loader.LoadConfig(configPaths...); err != nil { return err } @@ -37,36 +24,6 @@ func LoadConfig(configPaths ...string) error { return nil } -// LoadConfigFromFolder loads all YAML files from the specified folder. -// This is a convenience wrapper around LoadConfig. -// -// Example: -// -// if err := lokstra_registry.LoadConfigFromFolder("config"); err != nil { -// logger.LogPanic(err) -// } -func LoadConfigFromFolder(configFolder string) error { - // Load all YAML files in the specified config folder - basePath := utils.GetBasePath() - configFolder = filepath.Join(basePath, configFolder) - files, err := filepath.Glob(filepath.Join(configFolder, "*.yaml")) - if err != nil { - return err - } - - if len(files) == 0 { - logger.LogInfo("⚠️ No YAML config found in folder: %s", configFolder) - return nil - } - - lenPrefix := len(basePath) + 1 - for i, f := range files { - files[i] = f[lenPrefix:] - } - - return LoadConfig(files...) -} - // LoadNamedDbPoolsFromConfig sets up database pools from loaded config. // Must be called AFTER LoadConfig() if you use named-db-pools in config. // Call this explicitly only if you need DB pools. @@ -114,3 +71,8 @@ func RunConfiguredServer() error { // Run server return RunServer(server, timeout) } + +// return runtime mode: dev, debug, or prod +func GetRuntimeMode() string { + return GetConfig("runtime.mode", "prod") +} diff --git a/lokstra_registry/registry.go b/lokstra_registry/registry.go index d74b77b8..e98b665c 100644 --- a/lokstra_registry/registry.go +++ b/lokstra_registry/registry.go @@ -277,6 +277,11 @@ func RegisterService(name string, instance any) { deploy.Global().RegisterService(name, instance) } +// UnregisterService removes a service from the runtime registry +func UnregisterService(name string) { + deploy.Global().UnregisterService(name) +} + // check if a service is registered in the global registry func HasService(name string) bool { return deploy.Global().HasService(name) diff --git a/loksttra_pgx_sync_config.go b/loksttra_pgx_sync_config.go deleted file mode 100644 index 2ab8207d..00000000 --- a/loksttra_pgx_sync_config.go +++ /dev/null @@ -1,8 +0,0 @@ -package lokstra - -import "github.com/primadi/lokstra/services/sync_config_pg" - -func UsePgxSyncConfig(syncDbPoolName string) { - // Register SyncConfigPG service type (SyncMap based) - sync_config_pg.Register(syncDbPoolName) -} diff --git a/project_templates/02_app_framework/01_medium_system/main.go b/project_templates/02_app_framework/01_medium_system/main.go index b4fdc62e..7432edfa 100644 --- a/project_templates/02_app_framework/01_medium_system/main.go +++ b/project_templates/02_app_framework/01_medium_system/main.go @@ -3,8 +3,8 @@ package main import ( "fmt" - "github.com/primadi/lokstra" "github.com/primadi/lokstra/common/logger" + "github.com/primadi/lokstra/lokstra_registry" ) func main() { @@ -32,11 +32,11 @@ func main() { registerMiddlewareTypes() // 3. Run server from config - if err := lokstra.LoadConfig(); err != nil { + if err := lokstra_registry.LoadConfig(); err != nil { logger.LogPanic("❌ Failed to load config:", err) } - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { logger.LogPanic("❌ Failed to run server:", err) } } diff --git a/project_templates/02_app_framework/02_enterprise_modular/main.go b/project_templates/02_app_framework/02_enterprise_modular/main.go index c7659729..bf706d14 100644 --- a/project_templates/02_app_framework/02_enterprise_modular/main.go +++ b/project_templates/02_app_framework/02_enterprise_modular/main.go @@ -3,8 +3,8 @@ package main import ( "fmt" - "github.com/primadi/lokstra" "github.com/primadi/lokstra/common/logger" + "github.com/primadi/lokstra/lokstra_registry" ) func main() { @@ -25,11 +25,11 @@ func main() { // 3. Run server from config folder // Lokstra will automatically merge all YAML files in config/ folder - if err := lokstra.LoadConfigFromFolder("config"); err != nil { + if err := lokstra_registry.LoadConfig("config"); err != nil { logger.LogPanic("❌ Failed to load config:", err) } - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { logger.LogPanic("❌ Failed to run server:", err) } } diff --git a/project_templates/02_app_framework/03_enterprise_router_service/README_FLOWS.md b/project_templates/02_app_framework/03_enterprise_router_service/README_FLOWS.md index e8bf6e3e..9747c54a 100644 --- a/project_templates/02_app_framework/03_enterprise_router_service/README_FLOWS.md +++ b/project_templates/02_app_framework/03_enterprise_router_service/README_FLOWS.md @@ -20,7 +20,7 @@ lokstra_registry.RunServerFromConfigFolder("config") ### New Flow ```go -lokstra.LoadConfigFromFolder("config") +lokstra_registry.LoadConfigFromFolder("config") registerServiceTypes() // Config available here! registerMiddlewareTypes() // Config available here! lokstra.InitAndRunServer() diff --git a/project_templates/02_app_framework/03_enterprise_router_service/main.go b/project_templates/02_app_framework/03_enterprise_router_service/main.go index 367a2c8b..2f8a68d0 100644 --- a/project_templates/02_app_framework/03_enterprise_router_service/main.go +++ b/project_templates/02_app_framework/03_enterprise_router_service/main.go @@ -3,17 +3,17 @@ package main import ( "fmt" - "github.com/primadi/lokstra" + "github.com/primadi/lokstra/lokstra_init" ) // NEW RECOMMENDED FLOW // This flow separates config loading from service registration, // allowing services to access config during registration. func main() { - if err := lokstra.BootstrapAndRun( + if err := lokstra_init.BootstrapAndRun( // lokstra.WithLogLevel(logger.LogLevelDebug), - lokstra.WithoutDbMigrations(), - lokstra.WithServerInitFunc(func() error { + lokstra_init.WithDbMigrations(false, "migrations"), + lokstra_init.WithServerInitFunc(func() error { fmt.Println("") fmt.Println("╔═══════════════════════════════════════════════╗") fmt.Println("║ LOKSTRA ENTERPRISE MODULAR TEMPLATE ║") diff --git a/project_templates/02_app_framework/03_enterprise_router_service/main_alt.go b/project_templates/02_app_framework/03_enterprise_router_service/main_alt.go index d5897f05..804077d5 100644 --- a/project_templates/02_app_framework/03_enterprise_router_service/main_alt.go +++ b/project_templates/02_app_framework/03_enterprise_router_service/main_alt.go @@ -3,12 +3,13 @@ package main import ( "fmt" - "github.com/primadi/lokstra" "github.com/primadi/lokstra/common/logger" + "github.com/primadi/lokstra/lokstra_init" + "github.com/primadi/lokstra/lokstra_registry" ) func AltMain() { - lokstra.Bootstrap() + lokstra_init.Bootstrap() fmt.Println("") fmt.Println("╔═══════════════════════════════════════════════╗") @@ -27,11 +28,11 @@ func AltMain() { // 3. Run server from config folder // Lokstra will automatically merge all YAML files in config/ folder - if err := lokstra.LoadConfigFromFolder("config"); err != nil { + if err := lokstra_registry.LoadConfig("config"); err != nil { logger.LogPanic("❌ Failed to load config:", err) } - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { logger.LogPanic("❌ Failed to run server:", err) } } diff --git a/project_templates/02_app_framework/04_sync_config/config/config.yaml b/project_templates/02_app_framework/04_sync_config/config/config.yaml index 7854b1b7..7e46c917 100644 --- a/project_templates/02_app_framework/04_sync_config/config/config.yaml +++ b/project_templates/02_app_framework/04_sync_config/config/config.yaml @@ -1,12 +1,5 @@ # yaml-language-server: $schema=https://primadi.github.io/lokstra/schema/lokstra.schema.json -configs: - dbpool-manager: - use_sync: true - # dbpool_name: db_main - # heartbeat_interval: ${DBPOOL_MANAGER_HEARTBEAT_INTERVAL:5} # in minutes - # reconnect_interval: ${DBPOOL_MANAGER_RECONNECT_INTERVAL:5} # in seconds - named-db-pools: db_main: dsn: ${GLOBAL_DB_DSN:postgres://postgres:adm1n@localhost:5432/lokstra_db} diff --git a/project_templates/02_app_framework/04_sync_config/main.go b/project_templates/02_app_framework/04_sync_config/main.go index c94d0189..7094ff5f 100644 --- a/project_templates/02_app_framework/04_sync_config/main.go +++ b/project_templates/02_app_framework/04_sync_config/main.go @@ -1,28 +1,19 @@ package main import ( - "github.com/primadi/lokstra" + "github.com/primadi/lokstra/lokstra_init" ) func main() { - // 1. Bootstrap Lokstra framework - lokstra.Bootstrap() - - // 2. Load application config - if err := lokstra.LoadConfigFromFolder("config"); err != nil { - panic(err) - } - - // 3. auto db migrations - if err := lokstra.CheckDbMigrationsAuto("migrations"); err != nil { - panic(err) - } - - // 4. Register routers - registerRouters() - - // 5. Run the server - if err := lokstra.RunConfiguredServer(); err != nil { - panic(err) - } + lokstra_init.BootstrapAndRun( + lokstra_init.WithAnnotations(true), + lokstra_init.WithYAMLConfigPath(true, "config"), + lokstra_init.WithPgSyncMap(true, "db_main"), + lokstra_init.WithDbPoolManager(true, true), + lokstra_init.WithDbMigrations(true, "migrations"), + lokstra_init.WithServerInitFunc(func() error { + registerRouters() + return nil + }), + ) } diff --git a/project_templates/02_app_framework/test/main.go b/project_templates/02_app_framework/test/main.go index f96218bf..28b654dd 100644 --- a/project_templates/02_app_framework/test/main.go +++ b/project_templates/02_app_framework/test/main.go @@ -2,22 +2,23 @@ package main import ( "github.com/primadi/lokstra" + "github.com/primadi/lokstra/lokstra_init" "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/syncmap" ) func main() { // 1. Bootstrap Lokstra framework - lokstra.Bootstrap() + lokstra_init.Bootstrap() // 2. Load application config - lokstra.LoadConfig("config.yaml") + lokstra_registry.LoadConfig("config.yaml") // 3. Register routers registerRouters() // 4. Run the server - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { panic(err) } } diff --git a/serviceapi/dbpool_manager.go b/serviceapi/dbpool_manager.go index 73be484f..3e49f9d5 100644 --- a/serviceapi/dbpool_manager.go +++ b/serviceapi/dbpool_manager.go @@ -4,7 +4,17 @@ import ( "context" ) +// DbPoolInfo holds database DSN, schema name, and RLS context +type DbPoolInfo struct { + Dsn string + Schema string + RlsContext map[string]string +} + type DbPoolManager interface { + // Get all named DbPools + GetAllNamedDbPools() map[string]*DbPoolInfo + // get or create DbPool for the given dsn GetDbPool(dsn, schema string, rlsContext map[string]string) (DbPool, error) diff --git a/services/dbpool_manager/dbpool_manager.go b/services/dbpool_manager/dbpool_manager.go index 73c09353..d79a62bf 100644 --- a/services/dbpool_manager/dbpool_manager.go +++ b/services/dbpool_manager/dbpool_manager.go @@ -5,20 +5,19 @@ import ( "errors" "sync" + "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/serviceapi" "github.com/primadi/lokstra/services/dbpool_pg" ) -// DbPoolInfo holds database DSN, schema name, and RLS context -type DbPoolInfo struct { - Dsn string - Schema string - RlsContext map[string]string +// Helper to get global registry (avoid circular import) +func getGlobalRegistry() *deploy.GlobalRegistry { + return deploy.Global() } type DbPoolManager struct { - pools map[string]serviceapi.DbPool // key: dsn - namedPools map[string]*DbPoolInfo // key: name + pools map[string]serviceapi.DbPool // key: dsn + namedPools map[string]*serviceapi.DbPoolInfo // key: name mu sync.RWMutex newPoolFunc func(dsn, schema string, rlsContext map[string]string) (serviceapi.DbPool, error) } @@ -100,19 +99,33 @@ func (p *DbPoolManager) GetNamedDbPoolInfo(name string) (string, string, map[str // RemoveNamedDbPool implements serviceapi.DbPoolManager. func (p *DbPoolManager) RemoveNamedDbPool(name string) { p.mu.Lock() - defer p.mu.Unlock() delete(p.namedPools, name) + p.mu.Unlock() + + // Unregister service from registry + if registry := getGlobalRegistry(); registry != nil { + registry.UnregisterService(name) + } } // SetNamedDbPool implements serviceapi.DbPoolManager. func (p *DbPoolManager) SetNamedDbPool(name string, dsn string, schema string, rlsContext map[string]string) { p.mu.Lock() defer p.mu.Unlock() - p.namedPools[name] = &DbPoolInfo{ + p.namedPools[name] = &serviceapi.DbPoolInfo{ Dsn: dsn, Schema: schema, RlsContext: rlsContext, } + + // Auto-register pool as a service (lazy, created on first access) + // This makes pools accessible via lokstra_registry.GetService[DbPool](name) + if registry := getGlobalRegistry(); registry != nil { + registry.RegisterLazyService(name, func() any { + pool, _ := p.GetNamedDbPool(name) + return pool + }, nil) + } } // Shutdown implements serviceapi.DbPoolManager. @@ -127,12 +140,22 @@ func (p *DbPoolManager) Shutdown() error { return nil } +func (p *DbPoolManager) GetAllNamedDbPools() map[string]*serviceapi.DbPoolInfo { + p.mu.RLock() + defer p.mu.RUnlock() + result := make(map[string]*serviceapi.DbPoolInfo) + for name, info := range p.namedPools { + result[name] = info + } + return result +} + var _ serviceapi.DbPoolManager = (*DbPoolManager)(nil) func NewPoolManager(newPoolFunc func(dsn, schema string, rlsContext map[string]string) (serviceapi.DbPool, error)) serviceapi.DbPoolManager { return &DbPoolManager{ pools: make(map[string]serviceapi.DbPool), - namedPools: make(map[string]*DbPoolInfo), + namedPools: make(map[string]*serviceapi.DbPoolInfo), newPoolFunc: newPoolFunc, } } diff --git a/services/dbpool_manager/sync_pool_manager.go b/services/dbpool_manager/sync_pool_manager.go index a72f75da..75fd45ef 100644 --- a/services/dbpool_manager/sync_pool_manager.go +++ b/services/dbpool_manager/sync_pool_manager.go @@ -11,10 +11,13 @@ import ( ) type SyncDbPoolManager struct { - pools map[string]serviceapi.DbPool // key: dsn - namedPools *syncmap.SyncMap[*DbPoolInfo] // key: name - mu sync.RWMutex - newPoolFunc func(dsn, schema string, rlsContext map[string]string) (serviceapi.DbPool, error) + pools map[string]serviceapi.DbPool // key: dsn + namedPools *syncmap.SyncMap[*serviceapi.DbPoolInfo] // key: name + syncMapName string // lazy init: sync map name + mu sync.RWMutex + newPoolFunc func(dsn, schema string, rlsContext map[string]string) (serviceapi.DbPool, error) + syncMapMu sync.Mutex // lazy init mutex + syncMapReady bool // lazy init flag } // AcquireConn implements serviceapi.DbPoolManager. @@ -41,8 +44,23 @@ func (p *SyncDbPoolManager) AcquireConn(ctx context.Context, dsn string, schema return dbPool.Acquire(ctx) } +// ensureSyncMapInitialized initializes the SyncMap lazily on first use +func (p *SyncDbPoolManager) ensureSyncMapInitialized() { + p.syncMapMu.Lock() + defer p.syncMapMu.Unlock() + + if p.syncMapReady { + return + } + + // Create SyncMap now that sync-config service should be available + p.namedPools = syncmap.NewSyncMap[*serviceapi.DbPoolInfo](p.syncMapName) + p.syncMapReady = true +} + // AcquireNamedConn implements serviceapi.DbPoolManager. func (p *SyncDbPoolManager) AcquireNamedConn(ctx context.Context, name string) (serviceapi.DbConn, error) { + p.ensureSyncMapInitialized() dbPoolInfo, ok := p.namedPools.Load(name) if !ok { return nil, errors.New("dbpool: named pool not found: " + name) @@ -69,6 +87,7 @@ func (p *SyncDbPoolManager) GetDbPool(dsn string, schema string, rlsContext map[ // GetNamedDbPool implements serviceapi.DbPoolManager. func (p *SyncDbPoolManager) GetNamedDbPool(name string) (serviceapi.DbPool, error) { + p.ensureSyncMapInitialized() dbPoolInfo, ok := p.namedPools.Load(name) if !ok { return nil, errors.New("dbpool: named pool not found: " + name) @@ -78,6 +97,7 @@ func (p *SyncDbPoolManager) GetNamedDbPool(name string) (serviceapi.DbPool, erro // GetNamedDbPoolInfo implements serviceapi.DbPoolManager. func (p *SyncDbPoolManager) GetNamedDbPoolInfo(name string) (string, string, map[string]string, error) { + p.ensureSyncMapInitialized() dbPoolInfo, ok := p.namedPools.Load(name) if !ok { return "", "", nil, errors.New("dbpool: named pool not found: " + name) @@ -87,16 +107,41 @@ func (p *SyncDbPoolManager) GetNamedDbPoolInfo(name string) (string, string, map // RemoveNamedDbPool implements serviceapi.DbPoolManager. func (p *SyncDbPoolManager) RemoveNamedDbPool(name string) { + p.ensureSyncMapInitialized() p.namedPools.Delete(context.Background(), name) + + // Unregister service from registry + if registry := getGlobalRegistry(); registry != nil { + registry.UnregisterService(name) + } } // SetNamedDbPool implements serviceapi.DbPoolManager. func (p *SyncDbPoolManager) SetNamedDbPool(name string, dsn string, schema string, rlsContext map[string]string) { - p.namedPools.Store(name, &DbPoolInfo{ + p.ensureSyncMapInitialized() + p.namedPools.Store(name, &serviceapi.DbPoolInfo{ Dsn: dsn, Schema: schema, RlsContext: rlsContext, }) + + // Auto-register pool as a service (lazy, created on first access) + // This makes pools accessible via lokstra_registry.GetService[DbPool](name) + if registry := getGlobalRegistry(); registry != nil { + registry.RegisterLazyService(name, func() any { + pool, _ := p.GetNamedDbPool(name) + return pool + }, nil) + } +} + +func (p *SyncDbPoolManager) GetAllNamedDbPools() map[string]*serviceapi.DbPoolInfo { + p.ensureSyncMapInitialized() + all, err := p.namedPools.All(context.Background()) + if err != nil { + return nil + } + return all } // Shutdown implements serviceapi.DbPoolManager. @@ -116,8 +161,9 @@ var _ serviceapi.DbPoolManager = (*SyncDbPoolManager)(nil) func NewSyncDbPoolManager(syncName string, newPoolFunc func(dsn, schema string, rlsContext map[string]string) (serviceapi.DbPool, error)) serviceapi.DbPoolManager { return &SyncDbPoolManager{ pools: make(map[string]serviceapi.DbPool), - namedPools: syncmap.NewSyncMap[*DbPoolInfo](syncName), + syncMapName: syncName, newPoolFunc: newPoolFunc, + // namedPools will be lazily initialized on first use } } diff --git a/services/dbpool_pg/module.go b/services/dbpool_pg/module.go index 145da0cf..194068cd 100644 --- a/services/dbpool_pg/module.go +++ b/services/dbpool_pg/module.go @@ -31,6 +31,9 @@ type Config struct { MaxIdleTime time.Duration `json:"max-idle-time" yaml:"max-idle-time"` MaxLifetime time.Duration `json:"max-lifetime" yaml:"max-lifetime"` SSLMode string `json:"sslmode" yaml:"sslmode"` + + Schema string `json:"schema" yaml:"schema"` + RlsContext map[string]string `json:"rls-context" yaml:"rls-context"` } func (cfg *Config) buildDSN() string { @@ -68,7 +71,7 @@ func (cfg *Config) GetFinalDSN() string { func Service(cfg *Config) *pgxPostgresPool { dsn := cfg.GetFinalDSN() - svc, err := NewPgxPostgresPool(dsn, "", nil) + svc, err := NewPgxPostgresPool(dsn, cfg.Schema, cfg.RlsContext) if err != nil { return nil } @@ -88,6 +91,8 @@ func ServiceFactory(params map[string]any) any { MaxIdleTime: utils.GetValueFromMap(params, "max_idle_time", 30*time.Minute), MaxLifetime: utils.GetValueFromMap(params, "max_lifetime", time.Hour), SSLMode: utils.GetValueFromMap(params, "sslmode", "disable"), + Schema: utils.GetValueFromMap(params, "schema", "public"), + RlsContext: utils.GetValueFromMap(params, "rls_context", map[string]string{}), } return Service(cfg) } diff --git a/services/email_smtp/example/README.md b/services/email_smtp/example/README.md index dfe17386..514aa2e5 100644 --- a/services/email_smtp/example/README.md +++ b/services/email_smtp/example/README.md @@ -11,7 +11,7 @@ func main() { lokstra.Bootstrap() // Load config - lokstra.LoadConfig("config.yaml") + lokstra_registry.LoadConfig("config.yaml") // Services auto-registered when server runs lokstra_registry.InitAndRunServer() @@ -49,7 +49,7 @@ func main() { lokstra_registry.RegisterServiceType("email-api-service", EmailAPIServiceFactory) // Load config and run (auto-registers services from deployments) - lokstra.LoadConfigFromFolder("configs") + lokstra_registry.LoadConfigFromFolder("configs") lokstra_registry.InitAndRunServer() } ``` diff --git a/services/email_smtp/example/main.go b/services/email_smtp/example/main.go index 7dd1e06c..bfda0303 100644 --- a/services/email_smtp/example/main.go +++ b/services/email_smtp/example/main.go @@ -9,6 +9,7 @@ import ( "github.com/primadi/lokstra" "github.com/primadi/lokstra/common/logger" + "github.com/primadi/lokstra/lokstra_init" "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/services/email_smtp" ) @@ -16,7 +17,7 @@ import ( func main() { // lokstra.SetLogLevel(lokstra.LogLevelDebug) - lokstra.Bootstrap() + lokstra_init.Bootstrap() fmt.Println("\n===========================================") fmt.Println("Email SMTP Service Example") @@ -31,7 +32,7 @@ func main() { fmt.Println("===========================================") // Load config and run server (auto-registers services from deployments) - if err := lokstra.LoadConfigFromFolder("configs"); err != nil { + if err := lokstra_registry.LoadConfig("configs"); err != nil { logger.LogPanic(err.Error()) } diff --git a/services/register_all.go b/services/register_all.go index 8ff89f05..68bbe5be 100644 --- a/services/register_all.go +++ b/services/register_all.go @@ -4,6 +4,8 @@ package services import ( // Core services + "time" + "github.com/primadi/lokstra/services/dbpool_pg" "github.com/primadi/lokstra/services/email_smtp" "github.com/primadi/lokstra/services/kvstore_redis" @@ -21,5 +23,5 @@ func RegisterAllServices() { metrics_prometheus.Register() dbpool_pg.Register() email_smtp.Register() - sync_config_pg.Register("db_main") + sync_config_pg.Register("db_main", 5*time.Minute, 5*time.Second) } diff --git a/services/sync_config_pg/config_test.yaml b/services/sync_config_pg/config_test.yaml index d9383d0b..da6d1ad7 100644 --- a/services/sync_config_pg/config_test.yaml +++ b/services/sync_config_pg/config_test.yaml @@ -1,11 +1,4 @@ # yaml-language-server: $schema=https://primadi.github.io/lokstra/schema/lokstra.schema.json - -configs: - dbpool-manager: - use-sync: true - dbpool-name: db_main - heartbeat-interval: ${DBPOOL_MANAGER_HEARTBEAT_INTERVAL:5} # in minutes - reconnect-interval: ${DBPOOL_MANAGER_RECONNECT_INTERVAL:5} # in seconds named-db-pools: db_main: diff --git a/services/sync_config_pg/module.go b/services/sync_config_pg/module.go index 2bcef5f3..74c61719 100644 --- a/services/sync_config_pg/module.go +++ b/services/sync_config_pg/module.go @@ -142,6 +142,16 @@ func NewSyncConfigPG(cfg *Config) (serviceapi.SyncConfig, error) { } func (s *syncConfigPG) Set(ctx context.Context, key string, value any) error { + // Check if value already exists and is unchanged (optimization) + s.mu.RLock() + existingValue, exists := s.cache[key] + s.mu.RUnlock() + + if exists && equal(existingValue, value) { + // Value unchanged - skip database write to reduce IO/network load + return nil + } + valueJSON, err := json.Marshal(value) if err != nil { return fmt.Errorf("failed to marshal value: %w", err) @@ -574,13 +584,13 @@ func ServiceFactory(mapCfg map[string]any) any { } // Register registers the SyncConfig service type -func Register(dbPoolName string) { +func Register(dbPoolName string, heartBeatInterval, reconnectInterval time.Duration) { lokstra_registry.RegisterServiceType(SERVICE_TYPE, ServiceFactory) - SetDefaultSyncConfigPG(dbPoolName) + SetDefaultSyncConfigPG(dbPoolName, heartBeatInterval, reconnectInterval) } // registers the default SyncConfigPG service -func SetDefaultSyncConfigPG(syncDbPoolName string) { +func SetDefaultSyncConfigPG(syncDbPoolName string, heartBeatInterval, reconnectInterval time.Duration) { if lokstra_registry.HasService("sync-config") { return // Already registered } @@ -593,10 +603,8 @@ func SetDefaultSyncConfigPG(syncDbPoolName string) { SyncOnMismatch: true, EnableNotification: true, - HeartbeatInterval: lokstra_registry.GetConfig( - "dbpool-manager.heartbeat-interval", 5*time.Minute), // 5 minutes - ReconnectInterval: lokstra_registry.GetConfig( - "dbpool-manager.reconnect-interval", 5*time.Second), // 5 seconds + HeartbeatInterval: heartBeatInterval, + ReconnectInterval: reconnectInterval, } svc, err := Service(cfg) if err != nil { diff --git a/services/sync_config_pg/module_test.go b/services/sync_config_pg/module_test.go index b16dc9af..53b9c958 100644 --- a/services/sync_config_pg/module_test.go +++ b/services/sync_config_pg/module_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/primadi/lokstra" + "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/services/sync_config_pg" ) @@ -14,7 +14,7 @@ var once sync.Once func loadConfig(t *testing.T) { once.Do(func() { - if err := lokstra.LoadConfig("config_test.yaml"); err != nil { + if err := lokstra_registry.LoadConfig("config_test.yaml"); err != nil { t.Fatalf("Failed to load config: %v", err) } }) diff --git a/services/sync_config_pg/test/main.go b/services/sync_config_pg/test/main.go index 74647a2b..6c801e95 100644 --- a/services/sync_config_pg/test/main.go +++ b/services/sync_config_pg/test/main.go @@ -1,27 +1,31 @@ package main import ( + "time" + "github.com/primadi/lokstra" + "github.com/primadi/lokstra/lokstra_init" "github.com/primadi/lokstra/lokstra_registry" + "github.com/primadi/lokstra/services/sync_config_pg" "github.com/primadi/lokstra/syncmap" ) func main() { // 1. Bootstrap Lokstra framework - lokstra.Bootstrap() + lokstra_init.Bootstrap() // 2. Load application config - lokstra.LoadConfig("config.yaml") + lokstra_registry.LoadConfig("config.yaml") - lokstra.UsePgxDbPoolManager(true) - lokstra.UsePgxSyncConfig("db_main") - lokstra.LoadNamedDbPoolsFromConfig() + lokstra_init.UsePgxDbPoolManager(true) + sync_config_pg.Register("db_main", 5*time.Minute, 5*time.Second) + lokstra_registry.LoadNamedDbPoolsFromConfig() // 3. Register routers registerRouters() // 4. Run the server - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { panic(err) } } diff --git a/syncmap/syncmap.go b/syncmap/syncmap.go index ea5a87bc..739e2be0 100644 --- a/syncmap/syncmap.go +++ b/syncmap/syncmap.go @@ -51,7 +51,18 @@ func NewSyncMapWithConfig[V any](config serviceapi.SyncConfig, prefix string) *S subs: make(map[string]func(key string, value V)), } - // Subscribe to config changes and filter by prefix + // Load initial data from SyncConfig (data that already exists in database) + // This ensures existing database entries are available in the SyncMap + if allData, err := config.GetAll(context.Background()); err == nil { + for fullKey, value := range allData { + if _, ok := sm.stripPrefix(fullKey); ok { + // This key belongs to our prefix - process it + sm.handleNotification(fullKey, value) + } + } + } + + // Subscribe to config changes and filter by prefix (for future changes) config.Subscribe(func(fullKey string, value any) { sm.handleNotification(fullKey, value) }) @@ -64,6 +75,9 @@ func NewSyncMapWithConfig[V any](config serviceapi.SyncConfig, prefix string) *S // The backend implementation (PostgreSQL, Redis, etc.) is determined by the SyncConfig instance func NewSyncMap[V any](prefix string) *SyncMap[V] { syncConfigService := lokstra_registry.GetService[serviceapi.SyncConfig]("sync-config") + if syncConfigService == nil { + panic("SyncConfig service not found in registry") + } return NewSyncMapWithConfig[V](syncConfigService, prefix) } diff --git a/syncmap/syncmap_test.go b/syncmap/syncmap_test.go index c1f779e1..0efe0259 100644 --- a/syncmap/syncmap_test.go +++ b/syncmap/syncmap_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/primadi/lokstra" + "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/syncmap" ) @@ -15,7 +15,7 @@ var once sync.Once func loadConfig(t *testing.T) { once.Do(func() { - if err := lokstra.LoadConfig("../services/sync_config_pg/config_test.yaml"); err != nil { + if err := lokstra_registry.LoadConfig("../services/sync_config_pg/config_test.yaml"); err != nil { t.Fatalf("Failed to load config: %v", err) } }) diff --git a/tools/migration_runner/example/example_multi_db.go b/tools/migration_runner/example/example_multi_db.go index 98a6b510..217b931e 100644 --- a/tools/migration_runner/example/example_multi_db.go +++ b/tools/migration_runner/example/example_multi_db.go @@ -2,30 +2,33 @@ package main import ( "log" + "time" - "github.com/primadi/lokstra" + "github.com/primadi/lokstra/lokstra_init" + "github.com/primadi/lokstra/lokstra_registry" + "github.com/primadi/lokstra/services/sync_config_pg" ) // Example: Multi-database migration setup // This demonstrates how to use migration.yaml for different databases func main() { // Bootstrap Lokstra (loads config, detects runtime mode) - lokstra.Bootstrap() + lokstra_init.Bootstrap() // load database and other configurations - if err := lokstra.LoadConfigFromFolder("config"); err != nil { + if err := lokstra_registry.LoadConfig("config"); err != nil { log.Fatalf("Failed to load config: %v", err) } - lokstra.UsePgxDbPoolManager(true) + lokstra_init.UsePgxDbPoolManager(true) - lokstra.UsePgxSyncConfig("db_main") - lokstra.LoadNamedDbPoolsFromConfig() + sync_config_pg.Register("db_main", 5*time.Minute, 5*time.Second) + lokstra_registry.LoadNamedDbPoolsFromConfig() // OPTION 1: Auto-scan all migration folders (RECOMMENDED) // Scans multi_db/ for subdirectories, runs them in alphabetical order // Each folder loads its own migration.yaml configuration - if err := lokstra.CheckDbMigrationsAuto("multi_db"); err != nil { + if err := lokstra_init.CheckDbMigrationsAuto("multi_db"); err != nil { log.Fatalf("Multi-database migrations failed: %v", err) } @@ -57,7 +60,7 @@ func main() { // lokstra migration status -dir multi_db/ledger-db // Start your application servers - if err := lokstra.RunConfiguredServer(); err != nil { + if err := lokstra_registry.RunConfiguredServer(); err != nil { log.Fatalf("Failed to start server: %v", err) } } diff --git a/tools/migration_runner/example/example_with_lokstra.go b/tools/migration_runner/example/example_with_lokstra.go index b039d46c..6f11477a 100644 --- a/tools/migration_runner/example/example_with_lokstra.go +++ b/tools/migration_runner/example/example_with_lokstra.go @@ -3,59 +3,37 @@ package main import ( "log" - "github.com/primadi/lokstra" + "github.com/primadi/lokstra/lokstra_init" ) // Example of using lokstra.CheckDbMigration in your main application // This is the recommended way for automatic migration in development func ExampleUsage() { // Bootstrap Lokstra first - lokstra.Bootstrap() + lokstra_init.Bootstrap() // Option 1: Auto mode (RECOMMENDED for development) // - Runs migrations in dev/debug mode automatically // - Skips migrations in prod mode (use CLI: lokstra migration up) - err := lokstra.CheckDbMigration(&lokstra.MigrationConfig{ + err := lokstra_init.CheckDbMigration(&lokstra_init.MigrationConfig{ MigrationsDir: "migrations", - Force: lokstra.MigrationForceAuto, // or leave empty (default) }) if err != nil { log.Fatalf("Migration check failed: %v", err) } // Option 2: Always run (even in prod - NOT RECOMMENDED) - err = lokstra.CheckDbMigration(&lokstra.MigrationConfig{ + err = lokstra_init.CheckDbMigration(&lokstra_init.MigrationConfig{ MigrationsDir: "migrations", - Force: lokstra.MigrationForceTrue, }) if err != nil { log.Fatalf("Migration failed: %v", err) } - // Option 3: Never run (use CLI only) - err = lokstra.CheckDbMigration(&lokstra.MigrationConfig{ - MigrationsDir: "migrations", - Force: lokstra.MigrationForceFalse, - }) - if err != nil { - log.Fatalf("Migration check failed: %v", err) - } - - // Option 4: Custom database pool - err = lokstra.CheckDbMigration(&lokstra.MigrationConfig{ + // Option 3: Custom database pool + err = lokstra_init.CheckDbMigration(&lokstra_init.MigrationConfig{ MigrationsDir: "db/migrations", DbPoolName: "analytics-db", // from config.yaml named-db-pools - Force: lokstra.MigrationForceAuto, - }) - if err != nil { - log.Fatalf("Migration failed: %v", err) - } - - // Option 5: Silent mode (no logs) - err = lokstra.CheckDbMigration(&lokstra.MigrationConfig{ - MigrationsDir: "migrations", - Force: lokstra.MigrationForceAuto, - Silent: true, }) if err != nil { log.Fatalf("Migration failed: %v", err) @@ -68,10 +46,10 @@ func ExampleUsage() { // Typical usage in a real application func TypicalMain() { // 1. Bootstrap Lokstra (loads config, detects mode, etc) - lokstra.Bootstrap() + lokstra_init.Bootstrap() // 2. Auto-run migrations in dev/debug, skip in prod - if err := lokstra.CheckDbMigration(nil); err != nil { + if err := lokstra_init.CheckDbMigration(nil); err != nil { log.Fatalf("Migration error: %v", err) } diff --git a/tools/migration_runner/example/main.go b/tools/migration_runner/example/main.go index 434114d7..996ed033 100644 --- a/tools/migration_runner/example/main.go +++ b/tools/migration_runner/example/main.go @@ -7,7 +7,7 @@ import ( "log" "os" - "github.com/primadi/lokstra" + "github.com/primadi/lokstra/lokstra_init" "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/serviceapi" "github.com/primadi/lokstra/tools/migration_runner" @@ -40,8 +40,8 @@ func MainTest() { } // Bootstrap and load config for other commands - lokstra.Bootstrap() - lokstra_registry.LoadConfigFromFolder("config") + lokstra_init.Bootstrap() + lokstra_registry.LoadConfig("config") // Get database pool pool, ok := lokstra_registry.GetServiceAny(*dbName)