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
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"github.com/insertr/insertr/internal/content"
|
"github.com/insertr/insertr/internal/content"
|
||||||
|
"github.com/insertr/insertr/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
var enhanceCmd = &cobra.Command{
|
var enhanceCmd = &cobra.Command{
|
||||||
@@ -23,16 +24,13 @@ process that transforms static HTML into an editable CMS.`,
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
outputDir string
|
outputDir string
|
||||||
mockContent bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
enhanceCmd.Flags().StringVarP(&outputDir, "output", "o", "./dist", "Output directory for enhanced files")
|
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
|
// Bind flags to viper
|
||||||
viper.BindPFlag("build.output", enhanceCmd.Flags().Lookup("output"))
|
viper.BindPFlag("cli.output", enhanceCmd.Flags().Lookup("output"))
|
||||||
viper.BindPFlag("mock_content", enhanceCmd.Flags().Lookup("mock"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runEnhance(cmd *cobra.Command, args []string) {
|
func runEnhance(cmd *cobra.Command, args []string) {
|
||||||
@@ -47,21 +45,24 @@ func runEnhance(cmd *cobra.Command, args []string) {
|
|||||||
dbPath := viper.GetString("database.path")
|
dbPath := viper.GetString("database.path")
|
||||||
apiURL := viper.GetString("api.url")
|
apiURL := viper.GetString("api.url")
|
||||||
apiKey := viper.GetString("api.key")
|
apiKey := viper.GetString("api.key")
|
||||||
siteID := viper.GetString("site_id")
|
siteID := viper.GetString("cli.site_id")
|
||||||
mockContent := viper.GetBool("mock_content")
|
outputDir := viper.GetString("cli.output")
|
||||||
|
|
||||||
// Create content client
|
// Create content client
|
||||||
var client content.ContentClient
|
var client content.ContentClient
|
||||||
if mockContent || (apiURL == "" && dbPath == "") {
|
if apiURL != "" {
|
||||||
fmt.Printf("🧪 Using mock content for development\n")
|
|
||||||
client = content.NewMockClient()
|
|
||||||
} else if apiURL != "" {
|
|
||||||
fmt.Printf("🌐 Using content API: %s\n", apiURL)
|
fmt.Printf("🌐 Using content API: %s\n", apiURL)
|
||||||
client = content.NewHTTPClient(apiURL, apiKey)
|
client = content.NewHTTPClient(apiURL, apiKey)
|
||||||
} else {
|
} else if dbPath != "" {
|
||||||
fmt.Printf("🗄️ Using database: %s\n", dbPath)
|
fmt.Printf("🗄️ Using database: %s\n", dbPath)
|
||||||
// TODO: Implement database client for direct DB access
|
database, err := db.NewDatabase(dbPath)
|
||||||
fmt.Printf("⚠️ Direct database access not yet implemented, using mock content\n")
|
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()
|
client = content.NewMockClient()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func init() {
|
|||||||
viper.BindPFlag("database.path", rootCmd.PersistentFlags().Lookup("db"))
|
viper.BindPFlag("database.path", rootCmd.PersistentFlags().Lookup("db"))
|
||||||
viper.BindPFlag("api.url", rootCmd.PersistentFlags().Lookup("api-url"))
|
viper.BindPFlag("api.url", rootCmd.PersistentFlags().Lookup("api-url"))
|
||||||
viper.BindPFlag("api.key", rootCmd.PersistentFlags().Lookup("api-key"))
|
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(enhanceCmd)
|
||||||
rootCmd.AddCommand(serveCmd)
|
rootCmd.AddCommand(serveCmd)
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ func init() {
|
|||||||
|
|
||||||
// Bind flags to viper
|
// Bind flags to viper
|
||||||
viper.BindPFlag("server.port", serveCmd.Flags().Lookup("port"))
|
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) {
|
func runServe(cmd *cobra.Command, args []string) {
|
||||||
// Get configuration values
|
// Get configuration values
|
||||||
port := viper.GetInt("server.port")
|
port := viper.GetInt("server.port")
|
||||||
dbPath := viper.GetString("database.path")
|
dbPath := viper.GetString("database.path")
|
||||||
devMode := viper.GetBool("server.dev_mode")
|
devMode := viper.GetBool("dev_mode")
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database
|
||||||
database, err := db.NewDatabase(dbPath)
|
database, err := db.NewDatabase(dbPath)
|
||||||
|
|||||||
32
insertr.yaml
32
insertr.yaml
@@ -1,25 +1,23 @@
|
|||||||
# Insertr Configuration File
|
# Insertr Unified Configuration
|
||||||
# This file provides default configuration for the unified insertr binary
|
# Server and CLI configuration - library manages its own config
|
||||||
|
|
||||||
|
# Global settings
|
||||||
|
dev_mode: false # Development mode (affects server CORS, CLI verbosity)
|
||||||
|
|
||||||
# Database configuration
|
# Database configuration
|
||||||
database:
|
database:
|
||||||
path: "./insertr.db" # SQLite file path or PostgreSQL connection string
|
path: "./insertr.db" # SQLite file or PostgreSQL connection string
|
||||||
|
|
||||||
# API configuration (for remote content API)
|
# Server configuration (multi-site ready)
|
||||||
api:
|
|
||||||
url: "" # Content API URL (leave empty to use local database)
|
|
||||||
key: "" # API authentication key
|
|
||||||
|
|
||||||
# Server configuration
|
|
||||||
server:
|
server:
|
||||||
port: 8080 # HTTP server port
|
port: 8080 # HTTP API server port
|
||||||
dev_mode: false # Enable development mode features
|
|
||||||
|
|
||||||
# Build configuration
|
# CLI enhancement configuration
|
||||||
build:
|
cli:
|
||||||
input: "./src" # Default input directory for enhancement
|
site_id: "demo" # Default site ID for CLI operations
|
||||||
output: "./dist" # Default output directory for enhanced files
|
output: "./dist" # Default output directory for enhanced files
|
||||||
|
|
||||||
# Global settings
|
# API client configuration (for CLI remote mode)
|
||||||
site_id: "demo" # Default site ID for content lookup
|
api:
|
||||||
mock_content: false # Use mock content instead of real data
|
url: "" # Content API URL (empty = use local database)
|
||||||
|
key: "" # API authentication key
|
||||||
172
internal/content/database.go
Normal file
172
internal/content/database.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
10
justfile
10
justfile
@@ -34,7 +34,7 @@ dev: build-lib build
|
|||||||
|
|
||||||
# Start API server with prefixed output
|
# Start API server with prefixed output
|
||||||
echo "🔌 Starting API server (localhost:8080)..."
|
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=$!
|
SERVER_PID=$!
|
||||||
|
|
||||||
# Wait for server startup
|
# Wait for server startup
|
||||||
@@ -70,7 +70,7 @@ demo-only:
|
|||||||
dev-about: build-lib build
|
dev-about: build-lib build
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
echo "🚀 Starting full-stack development (about page)..."
|
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=$!
|
SERVER_PID=$!
|
||||||
sleep 3
|
sleep 3
|
||||||
npx --prefer-offline live-server demo-site --port=3000 --host=localhost --open=/about.html
|
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)
|
# Start content API server (default port 8080)
|
||||||
serve 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
|
# Start API server in production mode
|
||||||
serve-prod port="8080" db="./insertr.db":
|
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
|
# Start API server with auto-restart on Go file changes
|
||||||
serve-dev port="8080":
|
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
|
# Check API server health
|
||||||
health port="8080":
|
health port="8080":
|
||||||
|
|||||||
Reference in New Issue
Block a user