From c572428e4547cbee18ac69055a04b31368cdd211 Mon Sep 17 00:00:00 2001 From: Joakim Date: Wed, 10 Sep 2025 19:28:59 +0200 Subject: [PATCH] config: unify configuration with multi-site support - Remove mock_content setting (working database loop makes it unnecessary) - Change server.dev_mode to global dev_mode setting for consistency - Update CLI to use cli.site_id and cli.output for scoped configuration - Implement database client for CLI enhance command (complete static site loop) - Update justfile to use INSERTR_DATABASE_PATH environment variable - Enable multi-site architecture: server is site-agnostic, CLI is site-specific - Unified insertr.yaml now supports both server and CLI with minimal config --- cmd/enhance.go | 29 +++--- cmd/root.go | 2 +- cmd/serve.go | 4 +- insertr.yaml | 44 +++++---- internal/content/database.go | 172 +++++++++++++++++++++++++++++++++++ justfile | 10 +- 6 files changed, 216 insertions(+), 45 deletions(-) create mode 100644 internal/content/database.go diff --git a/cmd/enhance.go b/cmd/enhance.go index 835a427..d448567 100644 --- a/cmd/enhance.go +++ b/cmd/enhance.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/viper" "github.com/insertr/insertr/internal/content" + "github.com/insertr/insertr/internal/db" ) var enhanceCmd = &cobra.Command{ @@ -22,17 +23,14 @@ process that transforms static HTML into an editable CMS.`, } var ( - outputDir string - mockContent bool + outputDir string ) func init() { enhanceCmd.Flags().StringVarP(&outputDir, "output", "o", "./dist", "Output directory for enhanced files") - enhanceCmd.Flags().BoolVar(&mockContent, "mock", true, "Use mock content for development") // Bind flags to viper - viper.BindPFlag("build.output", enhanceCmd.Flags().Lookup("output")) - viper.BindPFlag("mock_content", enhanceCmd.Flags().Lookup("mock")) + viper.BindPFlag("cli.output", enhanceCmd.Flags().Lookup("output")) } func runEnhance(cmd *cobra.Command, args []string) { @@ -47,21 +45,24 @@ func runEnhance(cmd *cobra.Command, args []string) { dbPath := viper.GetString("database.path") apiURL := viper.GetString("api.url") apiKey := viper.GetString("api.key") - siteID := viper.GetString("site_id") - mockContent := viper.GetBool("mock_content") + siteID := viper.GetString("cli.site_id") + outputDir := viper.GetString("cli.output") // Create content client var client content.ContentClient - if mockContent || (apiURL == "" && dbPath == "") { - fmt.Printf("๐Ÿงช Using mock content for development\n") - client = content.NewMockClient() - } else if apiURL != "" { + if apiURL != "" { fmt.Printf("๐ŸŒ Using content API: %s\n", apiURL) client = content.NewHTTPClient(apiURL, apiKey) - } else { + } else if dbPath != "" { fmt.Printf("๐Ÿ—„๏ธ Using database: %s\n", dbPath) - // TODO: Implement database client for direct DB access - fmt.Printf("โš ๏ธ Direct database access not yet implemented, using mock content\n") + database, err := db.NewDatabase(dbPath) + if err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + defer database.Close() + client = content.NewDatabaseClient(database) + } else { + fmt.Printf("๐Ÿงช No database or API configured, using mock content\n") client = content.NewMockClient() } diff --git a/cmd/root.go b/cmd/root.go index 385a8c5..7913104 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -48,7 +48,7 @@ func init() { viper.BindPFlag("database.path", rootCmd.PersistentFlags().Lookup("db")) viper.BindPFlag("api.url", rootCmd.PersistentFlags().Lookup("api-url")) viper.BindPFlag("api.key", rootCmd.PersistentFlags().Lookup("api-key")) - viper.BindPFlag("site_id", rootCmd.PersistentFlags().Lookup("site-id")) + viper.BindPFlag("cli.site_id", rootCmd.PersistentFlags().Lookup("site-id")) rootCmd.AddCommand(enhanceCmd) rootCmd.AddCommand(serveCmd) diff --git a/cmd/serve.go b/cmd/serve.go index b232a93..666cd99 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -35,14 +35,14 @@ func init() { // Bind flags to viper viper.BindPFlag("server.port", serveCmd.Flags().Lookup("port")) - viper.BindPFlag("server.dev_mode", serveCmd.Flags().Lookup("dev-mode")) + viper.BindPFlag("dev_mode", serveCmd.Flags().Lookup("dev-mode")) } func runServe(cmd *cobra.Command, args []string) { // Get configuration values port := viper.GetInt("server.port") dbPath := viper.GetString("database.path") - devMode := viper.GetBool("server.dev_mode") + devMode := viper.GetBool("dev_mode") // Initialize database database, err := db.NewDatabase(dbPath) diff --git a/insertr.yaml b/insertr.yaml index 2765e4f..fc5a362 100644 --- a/insertr.yaml +++ b/insertr.yaml @@ -1,25 +1,23 @@ -# Insertr Configuration File -# This file provides default configuration for the unified insertr binary - -# Database configuration -database: - path: "./insertr.db" # SQLite file path or PostgreSQL connection string - -# API configuration (for remote content API) -api: - url: "" # Content API URL (leave empty to use local database) - key: "" # API authentication key - -# Server configuration -server: - port: 8080 # HTTP server port - dev_mode: false # Enable development mode features - -# Build configuration -build: - input: "./src" # Default input directory for enhancement - output: "./dist" # Default output directory for enhanced files +# Insertr Unified Configuration +# Server and CLI configuration - library manages its own config # Global settings -site_id: "demo" # Default site ID for content lookup -mock_content: false # Use mock content instead of real data \ No newline at end of file +dev_mode: false # Development mode (affects server CORS, CLI verbosity) + +# Database configuration +database: + path: "./insertr.db" # SQLite file or PostgreSQL connection string + +# Server configuration (multi-site ready) +server: + port: 8080 # HTTP API server port + +# CLI enhancement configuration +cli: + site_id: "demo" # Default site ID for CLI operations + output: "./dist" # Default output directory for enhanced files + +# API client configuration (for CLI remote mode) +api: + url: "" # Content API URL (empty = use local database) + key: "" # API authentication key \ No newline at end of file diff --git a/internal/content/database.go b/internal/content/database.go new file mode 100644 index 0000000..4f2003d --- /dev/null +++ b/internal/content/database.go @@ -0,0 +1,172 @@ +package content + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/insertr/insertr/internal/db" + "github.com/insertr/insertr/internal/db/postgresql" + "github.com/insertr/insertr/internal/db/sqlite" +) + +// DatabaseClient implements ContentClient for direct database access +type DatabaseClient struct { + db *db.Database +} + +// NewDatabaseClient creates a new database content client +func NewDatabaseClient(database *db.Database) *DatabaseClient { + return &DatabaseClient{ + db: database, + } +} + +// GetContent fetches a single content item by ID +func (d *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, error) { + ctx := context.Background() + var content interface{} + var err error + + switch d.db.GetDBType() { + case "sqlite3": + content, err = d.db.GetSQLiteQueries().GetContent(ctx, sqlite.GetContentParams{ + ID: contentID, + SiteID: siteID, + }) + case "postgresql": + content, err = d.db.GetPostgreSQLQueries().GetContent(ctx, postgresql.GetContentParams{ + ID: contentID, + SiteID: siteID, + }) + default: + return nil, fmt.Errorf("unsupported database type: %s", d.db.GetDBType()) + } + + if err != nil { + if err == sql.ErrNoRows { + return nil, nil // Content not found, return nil without error + } + return nil, fmt.Errorf("database error: %w", err) + } + + item := d.convertToContentItem(content) + return &item, nil +} + +// GetBulkContent fetches multiple content items by IDs +func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) { + if len(contentIDs) == 0 { + return make(map[string]ContentItem), nil + } + + ctx := context.Background() + var dbContent interface{} + var err error + + switch d.db.GetDBType() { + case "sqlite3": + dbContent, err = d.db.GetSQLiteQueries().GetBulkContent(ctx, sqlite.GetBulkContentParams{ + SiteID: siteID, + Ids: contentIDs, + }) + case "postgresql": + dbContent, err = d.db.GetPostgreSQLQueries().GetBulkContent(ctx, postgresql.GetBulkContentParams{ + SiteID: siteID, + Ids: contentIDs, + }) + default: + return nil, fmt.Errorf("unsupported database type: %s", d.db.GetDBType()) + } + + if err != nil { + return nil, fmt.Errorf("database error: %w", err) + } + + items := d.convertToContentItemList(dbContent) + + // Convert slice to map for easy lookup + result := make(map[string]ContentItem) + for _, item := range items { + result[item.ID] = item + } + + return result, nil +} + +// GetAllContent fetches all content for a site +func (d *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, error) { + ctx := context.Background() + var dbContent interface{} + var err error + + switch d.db.GetDBType() { + case "sqlite3": + dbContent, err = d.db.GetSQLiteQueries().GetAllContent(ctx, siteID) + case "postgresql": + dbContent, err = d.db.GetPostgreSQLQueries().GetAllContent(ctx, siteID) + default: + return nil, fmt.Errorf("unsupported database type: %s", d.db.GetDBType()) + } + + if err != nil { + return nil, fmt.Errorf("database error: %w", err) + } + + items := d.convertToContentItemList(dbContent) + + // Convert slice to map for easy lookup + result := make(map[string]ContentItem) + for _, item := range items { + result[item.ID] = item + } + + return result, nil +} + +// convertToContentItem converts database models to content.ContentItem +func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem { + switch d.db.GetDBType() { + case "sqlite3": + c := content.(sqlite.Content) + return ContentItem{ + ID: c.ID, + SiteID: c.SiteID, + Value: c.Value, + Type: c.Type, + UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339), + } + case "postgresql": + c := content.(postgresql.Content) + return ContentItem{ + ID: c.ID, + SiteID: c.SiteID, + Value: c.Value, + Type: c.Type, + UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339), + } + } + return ContentItem{} // Should never happen +} + +// convertToContentItemList converts database model lists to content.ContentItem slice +func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []ContentItem { + switch d.db.GetDBType() { + case "sqlite3": + list := contentList.([]sqlite.Content) + items := make([]ContentItem, len(list)) + for i, content := range list { + items[i] = d.convertToContentItem(content) + } + return items + case "postgresql": + list := contentList.([]postgresql.Content) + items := make([]ContentItem, len(list)) + for i, content := range list { + items[i] = d.convertToContentItem(content) + } + return items + } + return []ContentItem{} // Should never happen +} diff --git a/justfile b/justfile index 193aeef..aad35a7 100644 --- a/justfile +++ b/justfile @@ -34,7 +34,7 @@ dev: build-lib build # Start API server with prefixed output echo "๐Ÿ”Œ Starting API server (localhost:8080)..." - ./insertr serve --dev-mode --db ./dev.db 2>&1 | sed 's/^/๐Ÿ”Œ [SERVER] /' & + INSERTR_DATABASE_PATH=./dev.db ./insertr serve --dev-mode 2>&1 | sed 's/^/๐Ÿ”Œ [SERVER] /' & SERVER_PID=$! # Wait for server startup @@ -70,7 +70,7 @@ demo-only: dev-about: build-lib build #!/usr/bin/env bash echo "๐Ÿš€ Starting full-stack development (about page)..." - ./insertr serve --dev-mode --db ./dev.db & + INSERTR_DATABASE_PATH=./dev.db ./insertr serve --dev-mode & SERVER_PID=$! sleep 3 npx --prefer-offline live-server demo-site --port=3000 --host=localhost --open=/about.html @@ -120,15 +120,15 @@ enhance input="demo-site" output="dist": # Start content API server (default port 8080) serve port="8080": - ./insertr serve --port {{port}} --dev-mode --db ./dev.db + INSERTR_DATABASE_PATH=./dev.db ./insertr serve --port {{port}} --dev-mode # Start API server in production mode serve-prod port="8080" db="./insertr.db": - ./insertr serve --port {{port}} --db {{db}} + INSERTR_DATABASE_PATH={{db}} ./insertr serve --port {{port}} # Start API server with auto-restart on Go file changes serve-dev port="8080": - find . -name "*.go" | entr -r ./insertr serve --port {{port}} --dev-mode --db ./dev.db + find . -name "*.go" | entr -r bash -c 'INSERTR_DATABASE_PATH=./dev.db ./insertr serve --port {{port}} --dev-mode' # Check API server health health port="8080":