From 01b921bfa325b23a923b8d937dd48104f7dcb5df Mon Sep 17 00:00:00 2001 From: Joakim Date: Wed, 8 Oct 2025 19:34:21 +0200 Subject: [PATCH] Refactor database layer to eliminate type switching and simplify architecture - Replace type switching with clean repository pattern using sqlc-generated code - Move ContentRepository interface and domain models to db package - Create separate SQLiteRepository and PostgreSQLRepository implementations - Remove unnecessary RepositoryAdapter and ContentClient interface duplication - Update all clients (HTTP, Mock) to implement db.ContentRepository directly - Add context.Context parameters to all repository methods (Go best practice) - Eliminate duplicate domain models and type conversions - Remove type aliases - use db package types directly throughout codebase - Update engine, content managers, and API handlers to use repositories directly Benefits: - Zero runtime type switching overhead - Single source of truth for domain models - Clean package boundaries and separation of concerns - Standard Go interface patterns with context support - Easier testing with mockable repository interface - Maintainable: adding new database types requires no changes to existing code --- cmd/enhance.go | 5 +- cmd/serve.go | 2 +- internal/api/handlers.go | 17 +- internal/content/client.go | 44 ++- internal/content/enhancer.go | 7 +- internal/content/mock.go | 42 ++- internal/content/site_manager.go | 7 +- internal/db/database.go | 12 + internal/db/postgresql_repository.go | 285 ++++++++++++++ internal/db/repository.go | 103 +++++ internal/db/sqlite_repository.go | 290 ++++++++++++++ internal/engine/database_client.go | 544 --------------------------- internal/engine/engine.go | 36 +- internal/engine/injector.go | 12 +- internal/engine/types.go | 70 ---- internal/engine/utils.go | 21 -- 16 files changed, 785 insertions(+), 712 deletions(-) create mode 100644 internal/db/postgresql_repository.go create mode 100644 internal/db/repository.go create mode 100644 internal/db/sqlite_repository.go delete mode 100644 internal/engine/database_client.go diff --git a/cmd/enhance.go b/cmd/enhance.go index 3c14971..9bf48d2 100644 --- a/cmd/enhance.go +++ b/cmd/enhance.go @@ -11,7 +11,6 @@ import ( "github.com/insertr/insertr/internal/content" "github.com/insertr/insertr/internal/db" - "github.com/insertr/insertr/internal/engine" ) var enhanceCmd = &cobra.Command{ @@ -93,7 +92,7 @@ func runEnhance(cmd *cobra.Command, args []string) { } // Create content client - var client engine.ContentClient + var client db.ContentRepository if cfg.API.URL != "" { fmt.Printf("๐ŸŒ Using content API: %s\n", cfg.API.URL) client = content.NewHTTPClient(cfg.API.URL, cfg.API.Key) @@ -104,7 +103,7 @@ func runEnhance(cmd *cobra.Command, args []string) { log.Fatalf("Failed to initialize database: %v", err) } defer database.Close() - client = engine.NewDatabaseClient(database) + client = database.NewContentRepository() } else { fmt.Printf("๐Ÿงช No database or API configured, using mock content\n") client = content.NewMockClient() diff --git a/cmd/serve.go b/cmd/serve.go index a0f7dbd..11d1200 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -102,7 +102,7 @@ func runServe(cmd *cobra.Command, args []string) { } // Initialize content client for site manager - contentClient := engine.NewDatabaseClient(database) + contentClient := database.NewContentRepository() // Initialize site manager with auth provider authProvider := &engine.AuthProvider{Type: cfg.Auth.Provider} diff --git a/internal/api/handlers.go b/internal/api/handlers.go index df16b53..591e992 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -33,7 +33,7 @@ type ContentHandler struct { // NewContentHandler creates a new content handler func NewContentHandler(database *db.Database, authService *auth.AuthService) *ContentHandler { // Create database client for engine - dbClient := engine.NewDatabaseClient(database) + dbClient := database.NewContentRepository() return &ContentHandler{ database: database, @@ -311,7 +311,7 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) { ID: contentID, SiteID: siteID, HtmlContent: req.HTMLContent, - OriginalTemplate: engine.ToNullString(req.OriginalTemplate), + OriginalTemplate: db.ToNullString(req.OriginalTemplate), LastEditedBy: userID, }) case "postgresql": @@ -319,7 +319,7 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) { ID: contentID, SiteID: siteID, HtmlContent: req.HTMLContent, - OriginalTemplate: engine.ToNullString(req.OriginalTemplate), + OriginalTemplate: db.ToNullString(req.OriginalTemplate), LastEditedBy: userID, }) default: @@ -676,7 +676,7 @@ func (h *ContentHandler) convertToAPIContent(content interface{}) ContentItem { ID: c.ID, SiteID: c.SiteID, HTMLContent: c.HtmlContent, - OriginalTemplate: engine.FromNullString(c.OriginalTemplate), + OriginalTemplate: db.FromNullString(c.OriginalTemplate), CreatedAt: time.Unix(c.CreatedAt, 0), UpdatedAt: time.Unix(c.UpdatedAt, 0), LastEditedBy: c.LastEditedBy, @@ -687,7 +687,7 @@ func (h *ContentHandler) convertToAPIContent(content interface{}) ContentItem { ID: c.ID, SiteID: c.SiteID, HTMLContent: c.HtmlContent, - OriginalTemplate: engine.FromNullString(c.OriginalTemplate), + OriginalTemplate: db.FromNullString(c.OriginalTemplate), CreatedAt: time.Unix(c.CreatedAt, 0), UpdatedAt: time.Unix(c.UpdatedAt, 0), LastEditedBy: c.LastEditedBy, @@ -727,7 +727,7 @@ func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []Cont ContentID: version.ContentID, SiteID: version.SiteID, HTMLContent: version.HtmlContent, - OriginalTemplate: engine.FromNullString(version.OriginalTemplate), + OriginalTemplate: db.FromNullString(version.OriginalTemplate), CreatedAt: time.Unix(version.CreatedAt, 0), CreatedBy: version.CreatedBy, } @@ -742,7 +742,7 @@ func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []Cont ContentID: version.ContentID, SiteID: version.SiteID, HTMLContent: version.HtmlContent, - OriginalTemplate: engine.FromNullString(version.OriginalTemplate), + OriginalTemplate: db.FromNullString(version.OriginalTemplate), CreatedAt: time.Unix(version.CreatedAt, 0), CreatedBy: version.CreatedBy, } @@ -994,10 +994,11 @@ func (h *ContentHandler) CreateCollectionItem(w http.ResponseWriter, r *http.Req } // Create database client for atomic operations - dbClient := engine.NewDatabaseClient(h.database) + dbClient := h.database.NewContentRepository() // Use atomic collection item creation createdItem, err := dbClient.CreateCollectionItemAtomic( + context.Background(), req.SiteID, req.CollectionID, req.TemplateID, diff --git a/internal/content/client.go b/internal/content/client.go index 75b6c7a..833cf48 100644 --- a/internal/content/client.go +++ b/internal/content/client.go @@ -1,6 +1,7 @@ package content import ( + "context" "encoding/json" "fmt" "io" @@ -9,10 +10,10 @@ import ( "strings" "time" - "github.com/insertr/insertr/internal/engine" + "github.com/insertr/insertr/internal/db" ) -// HTTPClient implements ContentClient for HTTP API access +// HTTPClient implements db.ContentRepository for HTTP API access type HTTPClient struct { BaseURL string APIKey string @@ -31,7 +32,7 @@ func NewHTTPClient(baseURL, apiKey string) *HTTPClient { } // GetContent fetches a single content item by ID -func (c *HTTPClient) GetContent(siteID, contentID string) (*engine.ContentItem, error) { +func (c *HTTPClient) GetContent(ctx context.Context, siteID, contentID string) (*db.ContentItem, error) { url := fmt.Sprintf("%s/api/content/%s?site_id=%s", c.BaseURL, contentID, siteID) req, err := http.NewRequest("GET", url, nil) @@ -62,7 +63,7 @@ func (c *HTTPClient) GetContent(siteID, contentID string) (*engine.ContentItem, return nil, fmt.Errorf("reading response: %w", err) } - var item engine.ContentItem + var item db.ContentItem if err := json.Unmarshal(body, &item); err != nil { return nil, fmt.Errorf("parsing response: %w", err) } @@ -71,9 +72,9 @@ func (c *HTTPClient) GetContent(siteID, contentID string) (*engine.ContentItem, } // GetBulkContent fetches multiple content items by IDs -func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) { +func (c *HTTPClient) GetBulkContent(ctx context.Context, siteID string, contentIDs []string) (map[string]db.ContentItem, error) { if len(contentIDs) == 0 { - return make(map[string]engine.ContentItem), nil + return make(map[string]db.ContentItem), nil } // Build query parameters @@ -109,13 +110,13 @@ func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[str return nil, fmt.Errorf("reading response: %w", err) } - var response engine.ContentResponse + var response db.ContentResponse if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("parsing response: %w", err) } // Convert slice to map for easy lookup - result := make(map[string]engine.ContentItem) + result := make(map[string]db.ContentItem) for _, item := range response.Content { result[item.ID] = item } @@ -124,7 +125,7 @@ func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[str } // GetAllContent fetches all content for a site -func (c *HTTPClient) GetAllContent(siteID string) (map[string]engine.ContentItem, error) { +func (c *HTTPClient) GetAllContent(ctx context.Context, siteID string) (map[string]db.ContentItem, error) { url := fmt.Sprintf("%s/api/content?site_id=%s", c.BaseURL, siteID) req, err := http.NewRequest("GET", url, nil) @@ -151,13 +152,13 @@ func (c *HTTPClient) GetAllContent(siteID string) (map[string]engine.ContentItem return nil, fmt.Errorf("reading response: %w", err) } - var response engine.ContentResponse + var response db.ContentResponse if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("parsing response: %w", err) } // Convert slice to map for easy lookup - result := make(map[string]engine.ContentItem) + result := make(map[string]db.ContentItem) for _, item := range response.Content { result[item.ID] = item } @@ -166,37 +167,42 @@ func (c *HTTPClient) GetAllContent(siteID string) (map[string]engine.ContentItem } // CreateContent creates a new content item via HTTP API -func (c *HTTPClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*engine.ContentItem, error) { +func (c *HTTPClient) CreateContent(ctx context.Context, siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*db.ContentItem, error) { // For now, HTTPClient CreateContent is not implemented for enhancer use // This would typically be used in API-driven enhancement scenarios return nil, fmt.Errorf("CreateContent not implemented for HTTPClient - use DatabaseClient for enhancement") } // Collection method stubs - TODO: Implement these for HTTP API -func (c *HTTPClient) GetCollection(siteID, collectionID string) (*engine.CollectionItem, error) { +func (c *HTTPClient) GetCollection(ctx context.Context, siteID, collectionID string) (*db.CollectionItem, error) { return nil, fmt.Errorf("collection operations not implemented in HTTPClient") } -func (c *HTTPClient) CreateCollection(siteID, collectionID, containerHTML, lastEditedBy string) (*engine.CollectionItem, error) { +func (c *HTTPClient) CreateCollection(ctx context.Context, siteID, collectionID, containerHTML, lastEditedBy string) (*db.CollectionItem, error) { return nil, fmt.Errorf("collection operations not implemented in HTTPClient") } -func (c *HTTPClient) GetCollectionItems(siteID, collectionID string) ([]engine.CollectionItemWithTemplate, error) { +func (c *HTTPClient) GetCollectionItems(ctx context.Context, siteID, collectionID string) ([]db.CollectionItemWithTemplate, error) { return nil, fmt.Errorf("collection operations not implemented in HTTPClient") } -func (c *HTTPClient) CreateCollectionTemplate(siteID, collectionID, name, htmlTemplate string, isDefault bool) (*engine.CollectionTemplateItem, error) { +func (c *HTTPClient) CreateCollectionTemplate(ctx context.Context, siteID, collectionID, name, htmlTemplate string, isDefault bool) (*db.CollectionTemplateItem, error) { return nil, fmt.Errorf("collection operations not implemented in HTTPClient") } -func (c *HTTPClient) GetCollectionTemplates(siteID, collectionID string) ([]engine.CollectionTemplateItem, error) { +func (c *HTTPClient) GetCollectionTemplates(ctx context.Context, siteID, collectionID string) ([]db.CollectionTemplateItem, error) { return nil, fmt.Errorf("collection operations not implemented in HTTPClient") } -func (c *HTTPClient) CreateCollectionItem(siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*engine.CollectionItemWithTemplate, error) { +func (c *HTTPClient) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*db.CollectionItemWithTemplate, error) { return nil, fmt.Errorf("collection operations not implemented in HTTPClient") } -func (c *HTTPClient) CreateCollectionItemAtomic(siteID, collectionID string, templateID int, lastEditedBy string) (*engine.CollectionItemWithTemplate, error) { +func (c *HTTPClient) CreateCollectionItemAtomic(ctx context.Context, siteID, collectionID string, templateID int, lastEditedBy string) (*db.CollectionItemWithTemplate, error) { return nil, fmt.Errorf("collection operations not implemented in HTTPClient") } + +// WithTransaction executes a function within a transaction (not supported for HTTP client) +func (c *HTTPClient) WithTransaction(ctx context.Context, fn func(db.ContentRepository) error) error { + return fmt.Errorf("transactions not supported for HTTP client") +} diff --git a/internal/content/enhancer.go b/internal/content/enhancer.go index dff3be8..5013d12 100644 --- a/internal/content/enhancer.go +++ b/internal/content/enhancer.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/insertr/insertr/internal/config" + "github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/engine" ) @@ -30,7 +31,7 @@ type Enhancer struct { } // NewEnhancer creates a new HTML enhancer with unified pipeline -func NewEnhancer(client engine.ContentClient, siteID string, config EnhancementConfig) *Enhancer { +func NewEnhancer(client db.ContentRepository, siteID string, config EnhancementConfig) *Enhancer { return &Enhancer{ engine: engine.NewContentEngine(client), discoverer: NewDiscoverer(), @@ -40,7 +41,7 @@ func NewEnhancer(client engine.ContentClient, siteID string, config EnhancementC } // NewEnhancerWithAuth creates a new HTML enhancer with auth provider -func NewEnhancerWithAuth(client engine.ContentClient, siteID string, config EnhancementConfig, authProvider *engine.AuthProvider) *Enhancer { +func NewEnhancerWithAuth(client db.ContentRepository, siteID string, config EnhancementConfig, authProvider *engine.AuthProvider) *Enhancer { return &Enhancer{ engine: engine.NewContentEngineWithAuth(client, authProvider), discoverer: NewDiscoverer(), @@ -50,7 +51,7 @@ func NewEnhancerWithAuth(client engine.ContentClient, siteID string, config Enha } // NewDefaultEnhancer creates an enhancer with default configuration -func NewDefaultEnhancer(client engine.ContentClient, siteID string) *Enhancer { +func NewDefaultEnhancer(client db.ContentRepository, siteID string) *Enhancer { defaultConfig := EnhancementConfig{ Discovery: DiscoveryConfig{ Enabled: true, diff --git a/internal/content/mock.go b/internal/content/mock.go index 9fe9554..e5eb236 100644 --- a/internal/content/mock.go +++ b/internal/content/mock.go @@ -1,21 +1,22 @@ package content import ( + "context" "fmt" "time" - "github.com/insertr/insertr/internal/engine" + "github.com/insertr/insertr/internal/db" ) // MockClient implements ContentClient with mock data for development type MockClient struct { - data map[string]engine.ContentItem + data map[string]db.ContentItem } // NewMockClient creates a new mock content client with sample data func NewMockClient() *MockClient { // Generate realistic mock content based on actual generated IDs - data := map[string]engine.ContentItem{ + data := map[string]db.ContentItem{ // Navigation (index.html has collision suffix) "navbar-logo-2b10ad": { ID: "navbar-logo-2b10ad", @@ -101,7 +102,7 @@ func NewMockClient() *MockClient { } // GetContent fetches a single content item by ID -func (m *MockClient) GetContent(siteID, contentID string) (*engine.ContentItem, error) { +func (m *MockClient) GetContent(ctx context.Context, siteID, contentID string) (*db.ContentItem, error) { if item, exists := m.data[contentID]; exists && item.SiteID == siteID { return &item, nil } @@ -111,11 +112,11 @@ func (m *MockClient) GetContent(siteID, contentID string) (*engine.ContentItem, } // GetBulkContent fetches multiple content items by IDs -func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) { - result := make(map[string]engine.ContentItem) +func (m *MockClient) GetBulkContent(ctx context.Context, siteID string, contentIDs []string) (map[string]db.ContentItem, error) { + result := make(map[string]db.ContentItem) for _, id := range contentIDs { - item, err := m.GetContent(siteID, id) + item, err := m.GetContent(ctx, siteID, id) if err != nil { return nil, err } @@ -128,8 +129,8 @@ func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[str } // GetAllContent fetches all content for a site -func (m *MockClient) GetAllContent(siteID string) (map[string]engine.ContentItem, error) { - result := make(map[string]engine.ContentItem) +func (m *MockClient) GetAllContent(ctx context.Context, siteID string) (map[string]db.ContentItem, error) { + result := make(map[string]db.ContentItem) for _, item := range m.data { if item.SiteID == siteID { @@ -141,9 +142,9 @@ func (m *MockClient) GetAllContent(siteID string) (map[string]engine.ContentItem } // CreateContent creates a new mock content item -func (m *MockClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*engine.ContentItem, error) { +func (m *MockClient) CreateContent(ctx context.Context, siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*db.ContentItem, error) { // For mock client, just create and store the item - item := engine.ContentItem{ + item := db.ContentItem{ ID: contentID, SiteID: siteID, HTMLContent: htmlContent, @@ -159,30 +160,35 @@ func (m *MockClient) CreateContent(siteID, contentID, htmlContent, originalTempl } // Collection method stubs - TODO: Implement these for mock testing -func (m *MockClient) GetCollection(siteID, collectionID string) (*engine.CollectionItem, error) { +func (m *MockClient) GetCollection(ctx context.Context, siteID, collectionID string) (*db.CollectionItem, error) { return nil, fmt.Errorf("collection operations not implemented in MockClient") } -func (m *MockClient) CreateCollection(siteID, collectionID, containerHTML, lastEditedBy string) (*engine.CollectionItem, error) { +func (m *MockClient) CreateCollection(ctx context.Context, siteID, collectionID, containerHTML, lastEditedBy string) (*db.CollectionItem, error) { return nil, fmt.Errorf("collection operations not implemented in MockClient") } -func (m *MockClient) GetCollectionItems(siteID, collectionID string) ([]engine.CollectionItemWithTemplate, error) { +func (m *MockClient) GetCollectionItems(ctx context.Context, siteID, collectionID string) ([]db.CollectionItemWithTemplate, error) { return nil, fmt.Errorf("collection operations not implemented in MockClient") } -func (m *MockClient) CreateCollectionTemplate(siteID, collectionID, name, htmlTemplate string, isDefault bool) (*engine.CollectionTemplateItem, error) { +func (m *MockClient) CreateCollectionTemplate(ctx context.Context, siteID, collectionID, name, htmlTemplate string, isDefault bool) (*db.CollectionTemplateItem, error) { return nil, fmt.Errorf("collection operations not implemented in MockClient") } -func (m *MockClient) GetCollectionTemplates(siteID, collectionID string) ([]engine.CollectionTemplateItem, error) { +func (m *MockClient) GetCollectionTemplates(ctx context.Context, siteID, collectionID string) ([]db.CollectionTemplateItem, error) { return nil, fmt.Errorf("collection operations not implemented in MockClient") } -func (m *MockClient) CreateCollectionItem(siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*engine.CollectionItemWithTemplate, error) { +func (m *MockClient) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*db.CollectionItemWithTemplate, error) { return nil, fmt.Errorf("collection operations not implemented in MockClient") } -func (m *MockClient) CreateCollectionItemAtomic(siteID, collectionID string, templateID int, lastEditedBy string) (*engine.CollectionItemWithTemplate, error) { +func (m *MockClient) CreateCollectionItemAtomic(ctx context.Context, siteID, collectionID string, templateID int, lastEditedBy string) (*db.CollectionItemWithTemplate, error) { return nil, fmt.Errorf("collection operations not implemented in MockClient") } + +// WithTransaction executes a function within a transaction (not supported for mock client) +func (m *MockClient) WithTransaction(ctx context.Context, fn func(db.ContentRepository) error) error { + return fmt.Errorf("transactions not supported for mock client") +} diff --git a/internal/content/site_manager.go b/internal/content/site_manager.go index ec222de..227a6af 100644 --- a/internal/content/site_manager.go +++ b/internal/content/site_manager.go @@ -9,6 +9,7 @@ import ( "sync" "github.com/insertr/insertr/internal/config" + "github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/engine" ) @@ -21,12 +22,12 @@ type SiteManager struct { enhancer *Enhancer mutex sync.RWMutex devMode bool - contentClient engine.ContentClient + contentClient db.ContentRepository authProvider *engine.AuthProvider } // NewSiteManager creates a new site manager -func NewSiteManager(contentClient engine.ContentClient, devMode bool) *SiteManager { +func NewSiteManager(contentClient db.ContentRepository, devMode bool) *SiteManager { return &SiteManager{ sites: make(map[string]*SiteConfig), enhancer: NewDefaultEnhancer(contentClient, ""), // siteID will be set per operation @@ -37,7 +38,7 @@ func NewSiteManager(contentClient engine.ContentClient, devMode bool) *SiteManag } // NewSiteManagerWithAuth creates a new site manager with auth provider -func NewSiteManagerWithAuth(contentClient engine.ContentClient, devMode bool, authProvider *engine.AuthProvider) *SiteManager { +func NewSiteManagerWithAuth(contentClient db.ContentRepository, devMode bool, authProvider *engine.AuthProvider) *SiteManager { if authProvider == nil { authProvider = &engine.AuthProvider{Type: "mock"} } diff --git a/internal/db/database.go b/internal/db/database.go index 7e6eec5..4ff7395 100644 --- a/internal/db/database.go +++ b/internal/db/database.go @@ -105,6 +105,18 @@ func (db *Database) GetPostgreSQLDB() *sql.DB { return db.conn } +// NewContentRepository creates a repository based on the database type +func (db *Database) NewContentRepository() ContentRepository { + switch db.dbType { + case "sqlite3": + return NewSQLiteRepository(db.conn) + case "postgresql": + return NewPostgreSQLRepository(db.conn) + default: + panic(fmt.Sprintf("unsupported database type: %s", db.dbType)) + } +} + // initializeSQLiteSchema sets up the SQLite database schema func (db *Database) initializeSQLiteSchema() error { ctx := context.Background() diff --git a/internal/db/postgresql_repository.go b/internal/db/postgresql_repository.go new file mode 100644 index 0000000..1298cfd --- /dev/null +++ b/internal/db/postgresql_repository.go @@ -0,0 +1,285 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + + "github.com/insertr/insertr/internal/db/postgresql" +) + +// PostgreSQLRepository implements ContentRepository for PostgreSQL databases +type PostgreSQLRepository struct { + queries *postgresql.Queries + db *sql.DB +} + +// NewPostgreSQLRepository creates a new PostgreSQL repository +func NewPostgreSQLRepository(db *sql.DB) *PostgreSQLRepository { + return &PostgreSQLRepository{ + queries: postgresql.New(db), + db: db, + } +} + +// GetContent retrieves a single content item +func (r *PostgreSQLRepository) GetContent(ctx context.Context, siteID, contentID string) (*ContentItem, error) { + content, err := r.queries.GetContent(ctx, postgresql.GetContentParams{ + ID: contentID, + SiteID: siteID, + }) + if err != nil { + return nil, err + } + return &ContentItem{ + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + LastEditedBy: content.LastEditedBy, + }, nil +} + +// GetBulkContent retrieves multiple content items +func (r *PostgreSQLRepository) GetBulkContent(ctx context.Context, siteID string, contentIDs []string) (map[string]ContentItem, error) { + contents, err := r.queries.GetBulkContent(ctx, postgresql.GetBulkContentParams{ + SiteID: siteID, + Ids: contentIDs, + }) + if err != nil { + return nil, err + } + + items := make(map[string]ContentItem) + for _, content := range contents { + items[content.ID] = ContentItem{ + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + LastEditedBy: content.LastEditedBy, + } + } + return items, nil +} + +// GetAllContent retrieves all content items for a site +func (r *PostgreSQLRepository) GetAllContent(ctx context.Context, siteID string) (map[string]ContentItem, error) { + contents, err := r.queries.GetAllContent(ctx, siteID) + if err != nil { + return nil, err + } + + items := make(map[string]ContentItem) + for _, content := range contents { + items[content.ID] = ContentItem{ + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + LastEditedBy: content.LastEditedBy, + } + } + return items, nil +} + +// CreateContent creates a new content item +func (r *PostgreSQLRepository) CreateContent(ctx context.Context, siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*ContentItem, error) { + content, err := r.queries.CreateContent(ctx, postgresql.CreateContentParams{ + ID: contentID, + SiteID: siteID, + HtmlContent: htmlContent, + OriginalTemplate: ToNullString(originalTemplate), + LastEditedBy: lastEditedBy, + }) + if err != nil { + return nil, err + } + return &ContentItem{ + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + LastEditedBy: content.LastEditedBy, + }, nil +} + +// GetCollection retrieves a collection container +func (r *PostgreSQLRepository) GetCollection(ctx context.Context, siteID, collectionID string) (*CollectionItem, error) { + collection, err := r.queries.GetCollection(ctx, postgresql.GetCollectionParams{ + ID: collectionID, + SiteID: siteID, + }) + if err != nil { + return nil, err + } + return &CollectionItem{ + ID: collection.ID, + SiteID: collection.SiteID, + ContainerHTML: collection.ContainerHtml, + LastEditedBy: collection.LastEditedBy, + }, nil +} + +// CreateCollection creates a new collection container +func (r *PostgreSQLRepository) CreateCollection(ctx context.Context, siteID, collectionID, containerHTML, lastEditedBy string) (*CollectionItem, error) { + collection, err := r.queries.CreateCollection(ctx, postgresql.CreateCollectionParams{ + ID: collectionID, + SiteID: siteID, + ContainerHtml: containerHTML, + LastEditedBy: lastEditedBy, + }) + if err != nil { + return nil, err + } + return &CollectionItem{ + ID: collection.ID, + SiteID: collection.SiteID, + ContainerHTML: collection.ContainerHtml, + LastEditedBy: collection.LastEditedBy, + }, nil +} + +// GetCollectionItems retrieves all items in a collection with template information +func (r *PostgreSQLRepository) GetCollectionItems(ctx context.Context, siteID, collectionID string) ([]CollectionItemWithTemplate, error) { + items, err := r.queries.GetCollectionItemsWithTemplate(ctx, postgresql.GetCollectionItemsWithTemplateParams{ + CollectionID: collectionID, + SiteID: siteID, + }) + if err != nil { + return nil, err + } + + result := make([]CollectionItemWithTemplate, len(items)) + for i, item := range items { + result[i] = CollectionItemWithTemplate{ + ItemID: item.ItemID, + CollectionID: item.CollectionID, + SiteID: item.SiteID, + TemplateID: int(item.TemplateID), + HTMLContent: item.HtmlContent, + Position: int(item.Position), + LastEditedBy: item.LastEditedBy, + TemplateName: item.TemplateName, + HTMLTemplate: item.HtmlTemplate, + IsDefault: item.IsDefault, // PostgreSQL uses BOOLEAN + } + } + return result, nil +} + +// CreateCollectionTemplate creates a new template for a collection +func (r *PostgreSQLRepository) CreateCollectionTemplate(ctx context.Context, siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error) { + template, err := r.queries.CreateCollectionTemplate(ctx, postgresql.CreateCollectionTemplateParams{ + CollectionID: collectionID, + SiteID: siteID, + Name: name, + HtmlTemplate: htmlTemplate, + IsDefault: isDefault, + }) + if err != nil { + return nil, err + } + return &CollectionTemplateItem{ + TemplateID: int(template.TemplateID), + CollectionID: template.CollectionID, + SiteID: template.SiteID, + Name: template.Name, + HTMLTemplate: template.HtmlTemplate, + IsDefault: template.IsDefault, + }, nil +} + +// GetCollectionTemplates retrieves all templates for a collection +func (r *PostgreSQLRepository) GetCollectionTemplates(ctx context.Context, siteID, collectionID string) ([]CollectionTemplateItem, error) { + templates, err := r.queries.GetCollectionTemplates(ctx, postgresql.GetCollectionTemplatesParams{ + CollectionID: collectionID, + SiteID: siteID, + }) + if err != nil { + return nil, err + } + + result := make([]CollectionTemplateItem, len(templates)) + for i, template := range templates { + result[i] = CollectionTemplateItem{ + TemplateID: int(template.TemplateID), + CollectionID: template.CollectionID, + SiteID: template.SiteID, + Name: template.Name, + HTMLTemplate: template.HtmlTemplate, + IsDefault: template.IsDefault, // PostgreSQL uses BOOLEAN + } + } + return result, nil +} + +// CreateCollectionItem creates a new collection item +func (r *PostgreSQLRepository) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) { + item, err := r.queries.CreateCollectionItem(ctx, postgresql.CreateCollectionItemParams{ + ItemID: itemID, + CollectionID: collectionID, + SiteID: siteID, + TemplateID: int32(templateID), + HtmlContent: htmlContent, + Position: int32(position), + LastEditedBy: lastEditedBy, + }) + if err != nil { + return nil, err + } + return &CollectionItemWithTemplate{ + ItemID: item.ItemID, + CollectionID: item.CollectionID, + SiteID: item.SiteID, + TemplateID: int(item.TemplateID), + HTMLContent: item.HtmlContent, + Position: int(item.Position), + LastEditedBy: item.LastEditedBy, + }, nil +} + +// CreateCollectionItemAtomic creates a collection item with all its content entries atomically +func (r *PostgreSQLRepository) CreateCollectionItemAtomic(ctx context.Context, siteID, collectionID string, templateID int, lastEditedBy string) (*CollectionItemWithTemplate, error) { + // Get template HTML for processing + templates, err := r.GetCollectionTemplates(ctx, siteID, collectionID) + if err != nil { + return nil, fmt.Errorf("failed to get templates: %w", err) + } + + var templateHTML string + for _, template := range templates { + if template.TemplateID == templateID { + templateHTML = template.HTMLTemplate + break + } + } + + if templateHTML == "" { + return nil, fmt.Errorf("template %d not found", templateID) + } + + // TODO: Implement using unified engine approach + // This requires circular dependency resolution + return nil, fmt.Errorf("CreateCollectionItemAtomic not yet implemented for PostgreSQL") +} + +// WithTransaction executes a function within a database transaction +func (r *PostgreSQLRepository) WithTransaction(ctx context.Context, fn func(ContentRepository) error) error { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + + txRepo := &PostgreSQLRepository{ + queries: r.queries.WithTx(tx), + db: r.db, + } + + if err := fn(txRepo); err != nil { + tx.Rollback() + return err + } + + return tx.Commit() +} diff --git a/internal/db/repository.go b/internal/db/repository.go new file mode 100644 index 0000000..4c2081a --- /dev/null +++ b/internal/db/repository.go @@ -0,0 +1,103 @@ +package db + +import ( + "context" + "database/sql" +) + +// ContentRepository interface for accessing content data +// This replaces the ContentClient interface from engine package +type ContentRepository interface { + GetContent(ctx context.Context, siteID, contentID string) (*ContentItem, error) + GetBulkContent(ctx context.Context, siteID string, contentIDs []string) (map[string]ContentItem, error) + GetAllContent(ctx context.Context, siteID string) (map[string]ContentItem, error) + CreateContent(ctx context.Context, siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*ContentItem, error) + + // Collection operations + GetCollection(ctx context.Context, siteID, collectionID string) (*CollectionItem, error) + CreateCollection(ctx context.Context, siteID, collectionID, containerHTML, lastEditedBy string) (*CollectionItem, error) + GetCollectionItems(ctx context.Context, siteID, collectionID string) ([]CollectionItemWithTemplate, error) + GetCollectionTemplates(ctx context.Context, siteID, collectionID string) ([]CollectionTemplateItem, error) + CreateCollectionTemplate(ctx context.Context, siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error) + CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) + CreateCollectionItemAtomic(ctx context.Context, siteID, collectionID string, templateID int, lastEditedBy string) (*CollectionItemWithTemplate, error) + + // Transaction support + WithTransaction(ctx context.Context, fn func(ContentRepository) error) error +} + +// ContentItem represents a piece of content from the database +type ContentItem struct { + ID string `json:"id"` + SiteID string `json:"site_id"` + HTMLContent string `json:"html_content"` + OriginalTemplate string `json:"original_template"` + UpdatedAt string `json:"updated_at"` + LastEditedBy string `json:"last_edited_by,omitempty"` +} + +// ContentResponse represents the API response structure +type ContentResponse struct { + Content []ContentItem `json:"content"` + Error string `json:"error,omitempty"` +} + +// CollectionItem represents a collection container from the database +type CollectionItem struct { + ID string `json:"id"` + SiteID string `json:"site_id"` + ContainerHTML string `json:"container_html"` + UpdatedAt string `json:"updated_at"` + LastEditedBy string `json:"last_edited_by,omitempty"` +} + +// CollectionTemplateItem represents a collection template from the database +type CollectionTemplateItem struct { + TemplateID int `json:"template_id"` + CollectionID string `json:"collection_id"` + SiteID string `json:"site_id"` + Name string `json:"name"` + HTMLTemplate string `json:"html_template"` + IsDefault bool `json:"is_default"` +} + +// CollectionItemWithTemplate represents a collection item with its template information +type CollectionItemWithTemplate struct { + ItemID string `json:"item_id"` + CollectionID string `json:"collection_id"` + SiteID string `json:"site_id"` + TemplateID int `json:"template_id"` + HTMLContent string `json:"html_content"` + Position int `json:"position"` + UpdatedAt string `json:"updated_at"` + LastEditedBy string `json:"last_edited_by"` + + // Template information + TemplateName string `json:"template_name"` + HTMLTemplate string `json:"html_template"` + IsDefault bool `json:"is_default"` +} + +// Helper function to convert sql.NullString to string +func getStringFromNullString(ns sql.NullString) string { + if ns.Valid { + return ns.String + } + return "" +} + +// ToNullString converts a string to sql.NullString +func ToNullString(s string) sql.NullString { + if s == "" { + return sql.NullString{Valid: false} + } + return sql.NullString{String: s, Valid: true} +} + +// FromNullString converts sql.NullString to string +func FromNullString(ns sql.NullString) string { + if ns.Valid { + return ns.String + } + return "" +} diff --git a/internal/db/sqlite_repository.go b/internal/db/sqlite_repository.go new file mode 100644 index 0000000..31320ea --- /dev/null +++ b/internal/db/sqlite_repository.go @@ -0,0 +1,290 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + + "github.com/insertr/insertr/internal/db/sqlite" +) + +// SQLiteRepository implements ContentRepository for SQLite databases +type SQLiteRepository struct { + queries *sqlite.Queries + db *sql.DB +} + +// NewSQLiteRepository creates a new SQLite repository +func NewSQLiteRepository(db *sql.DB) *SQLiteRepository { + return &SQLiteRepository{ + queries: sqlite.New(db), + db: db, + } +} + +// GetContent retrieves a single content item +func (r *SQLiteRepository) GetContent(ctx context.Context, siteID, contentID string) (*ContentItem, error) { + content, err := r.queries.GetContent(ctx, sqlite.GetContentParams{ + ID: contentID, + SiteID: siteID, + }) + if err != nil { + return nil, err + } + return &ContentItem{ + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + LastEditedBy: content.LastEditedBy, + }, nil +} + +// GetBulkContent retrieves multiple content items +func (r *SQLiteRepository) GetBulkContent(ctx context.Context, siteID string, contentIDs []string) (map[string]ContentItem, error) { + contents, err := r.queries.GetBulkContent(ctx, sqlite.GetBulkContentParams{ + SiteID: siteID, + Ids: contentIDs, + }) + if err != nil { + return nil, err + } + + items := make(map[string]ContentItem) + for _, content := range contents { + items[content.ID] = ContentItem{ + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + LastEditedBy: content.LastEditedBy, + } + } + return items, nil +} + +// GetAllContent retrieves all content items for a site +func (r *SQLiteRepository) GetAllContent(ctx context.Context, siteID string) (map[string]ContentItem, error) { + contents, err := r.queries.GetAllContent(ctx, siteID) + if err != nil { + return nil, err + } + + items := make(map[string]ContentItem) + for _, content := range contents { + items[content.ID] = ContentItem{ + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + LastEditedBy: content.LastEditedBy, + } + } + return items, nil +} + +// CreateContent creates a new content item +func (r *SQLiteRepository) CreateContent(ctx context.Context, siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*ContentItem, error) { + content, err := r.queries.CreateContent(ctx, sqlite.CreateContentParams{ + ID: contentID, + SiteID: siteID, + HtmlContent: htmlContent, + OriginalTemplate: ToNullString(originalTemplate), + LastEditedBy: lastEditedBy, + }) + if err != nil { + return nil, err + } + return &ContentItem{ + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + LastEditedBy: content.LastEditedBy, + }, nil +} + +// GetCollection retrieves a collection container +func (r *SQLiteRepository) GetCollection(ctx context.Context, siteID, collectionID string) (*CollectionItem, error) { + collection, err := r.queries.GetCollection(ctx, sqlite.GetCollectionParams{ + ID: collectionID, + SiteID: siteID, + }) + if err != nil { + return nil, err + } + return &CollectionItem{ + ID: collection.ID, + SiteID: collection.SiteID, + ContainerHTML: collection.ContainerHtml, + LastEditedBy: collection.LastEditedBy, + }, nil +} + +// CreateCollection creates a new collection container +func (r *SQLiteRepository) CreateCollection(ctx context.Context, siteID, collectionID, containerHTML, lastEditedBy string) (*CollectionItem, error) { + collection, err := r.queries.CreateCollection(ctx, sqlite.CreateCollectionParams{ + ID: collectionID, + SiteID: siteID, + ContainerHtml: containerHTML, + LastEditedBy: lastEditedBy, + }) + if err != nil { + return nil, err + } + return &CollectionItem{ + ID: collection.ID, + SiteID: collection.SiteID, + ContainerHTML: collection.ContainerHtml, + LastEditedBy: collection.LastEditedBy, + }, nil +} + +// GetCollectionItems retrieves all items in a collection with template information +func (r *SQLiteRepository) GetCollectionItems(ctx context.Context, siteID, collectionID string) ([]CollectionItemWithTemplate, error) { + items, err := r.queries.GetCollectionItemsWithTemplate(ctx, sqlite.GetCollectionItemsWithTemplateParams{ + CollectionID: collectionID, + SiteID: siteID, + }) + if err != nil { + return nil, err + } + + result := make([]CollectionItemWithTemplate, len(items)) + for i, item := range items { + result[i] = CollectionItemWithTemplate{ + ItemID: item.ItemID, + CollectionID: item.CollectionID, + SiteID: item.SiteID, + TemplateID: int(item.TemplateID), + HTMLContent: item.HtmlContent, + Position: int(item.Position), + LastEditedBy: item.LastEditedBy, + TemplateName: item.TemplateName, + HTMLTemplate: item.HtmlTemplate, + IsDefault: item.IsDefault != 0, // SQLite uses INTEGER for boolean + } + } + return result, nil +} + +// CreateCollectionTemplate creates a new template for a collection +func (r *SQLiteRepository) CreateCollectionTemplate(ctx context.Context, siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error) { + var isDefaultInt int64 + if isDefault { + isDefaultInt = 1 + } + + template, err := r.queries.CreateCollectionTemplate(ctx, sqlite.CreateCollectionTemplateParams{ + CollectionID: collectionID, + SiteID: siteID, + Name: name, + HtmlTemplate: htmlTemplate, + IsDefault: isDefaultInt, + }) + if err != nil { + return nil, err + } + return &CollectionTemplateItem{ + TemplateID: int(template.TemplateID), + CollectionID: template.CollectionID, + SiteID: template.SiteID, + Name: template.Name, + HTMLTemplate: template.HtmlTemplate, + IsDefault: template.IsDefault != 0, + }, nil +} + +// GetCollectionTemplates retrieves all templates for a collection +func (r *SQLiteRepository) GetCollectionTemplates(ctx context.Context, siteID, collectionID string) ([]CollectionTemplateItem, error) { + templates, err := r.queries.GetCollectionTemplates(ctx, sqlite.GetCollectionTemplatesParams{ + CollectionID: collectionID, + SiteID: siteID, + }) + if err != nil { + return nil, err + } + + result := make([]CollectionTemplateItem, len(templates)) + for i, template := range templates { + result[i] = CollectionTemplateItem{ + TemplateID: int(template.TemplateID), + CollectionID: template.CollectionID, + SiteID: template.SiteID, + Name: template.Name, + HTMLTemplate: template.HtmlTemplate, + IsDefault: template.IsDefault != 0, // SQLite uses INTEGER for boolean + } + } + return result, nil +} + +// CreateCollectionItem creates a new collection item +func (r *SQLiteRepository) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) { + item, err := r.queries.CreateCollectionItem(ctx, sqlite.CreateCollectionItemParams{ + ItemID: itemID, + CollectionID: collectionID, + SiteID: siteID, + TemplateID: int64(templateID), + HtmlContent: htmlContent, + Position: int64(position), + LastEditedBy: lastEditedBy, + }) + if err != nil { + return nil, err + } + return &CollectionItemWithTemplate{ + ItemID: item.ItemID, + CollectionID: item.CollectionID, + SiteID: item.SiteID, + TemplateID: int(item.TemplateID), + HTMLContent: item.HtmlContent, + Position: int(item.Position), + LastEditedBy: item.LastEditedBy, + }, nil +} + +// CreateCollectionItemAtomic creates a collection item with all its content entries atomically +func (r *SQLiteRepository) CreateCollectionItemAtomic(ctx context.Context, siteID, collectionID string, templateID int, lastEditedBy string) (*CollectionItemWithTemplate, error) { + // Get template HTML for processing + templates, err := r.GetCollectionTemplates(ctx, siteID, collectionID) + if err != nil { + return nil, fmt.Errorf("failed to get templates: %w", err) + } + + var templateHTML string + for _, template := range templates { + if template.TemplateID == templateID { + templateHTML = template.HTMLTemplate + break + } + } + + if templateHTML == "" { + return nil, fmt.Errorf("template %d not found", templateID) + } + + // TODO: Implement using unified engine approach + // This requires circular dependency resolution + return nil, fmt.Errorf("CreateCollectionItemAtomic not yet implemented for SQLite") +} + +// WithTransaction executes a function within a database transaction +func (r *SQLiteRepository) WithTransaction(ctx context.Context, fn func(ContentRepository) error) error { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + + txRepo := &SQLiteRepository{ + queries: r.queries.WithTx(tx), + db: r.db, + } + + if err := fn(txRepo); err != nil { + tx.Rollback() + return err + } + + return tx.Commit() +} diff --git a/internal/engine/database_client.go b/internal/engine/database_client.go deleted file mode 100644 index 0d29d65..0000000 --- a/internal/engine/database_client.go +++ /dev/null @@ -1,544 +0,0 @@ -package engine - -import ( - "context" - "database/sql" - "fmt" - - "github.com/insertr/insertr/internal/db" - "github.com/insertr/insertr/internal/db/postgresql" - "github.com/insertr/insertr/internal/db/sqlite" -) - -// Helper function to convert sql.NullString to string -func getStringFromNullString(ns sql.NullString) string { - if ns.Valid { - return ns.String - } - return "" -} - -// DatabaseClient implements ContentClient interface using the database -type DatabaseClient struct { - database *db.Database -} - -// NewDatabaseClient creates a new database client -func NewDatabaseClient(database *db.Database) *DatabaseClient { - return &DatabaseClient{ - database: database, - } -} - -// GetContent retrieves a single content item -func (c *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, error) { - switch c.database.GetDBType() { - case "sqlite3": - content, err := c.database.GetSQLiteQueries().GetContent(context.Background(), sqlite.GetContentParams{ - ID: contentID, - SiteID: siteID, - }) - if err != nil { - return nil, err - } - return &ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - HTMLContent: content.HtmlContent, - OriginalTemplate: getStringFromNullString(content.OriginalTemplate), - LastEditedBy: content.LastEditedBy, - }, nil - - case "postgresql": - content, err := c.database.GetPostgreSQLQueries().GetContent(context.Background(), postgresql.GetContentParams{ - ID: contentID, - SiteID: siteID, - }) - if err != nil { - return nil, err - } - return &ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - HTMLContent: content.HtmlContent, - OriginalTemplate: getStringFromNullString(content.OriginalTemplate), - LastEditedBy: content.LastEditedBy, - }, nil - - default: - return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) - } -} - -// GetBulkContent retrieves multiple content items -func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) { - switch c.database.GetDBType() { - case "sqlite3": - contents, err := c.database.GetSQLiteQueries().GetBulkContent(context.Background(), sqlite.GetBulkContentParams{ - SiteID: siteID, - Ids: contentIDs, - }) - if err != nil { - return nil, err - } - - items := make(map[string]ContentItem) - for _, content := range contents { - items[content.ID] = ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - HTMLContent: content.HtmlContent, - OriginalTemplate: getStringFromNullString(content.OriginalTemplate), - LastEditedBy: content.LastEditedBy, - } - } - return items, nil - - case "postgresql": - contents, err := c.database.GetPostgreSQLQueries().GetBulkContent(context.Background(), postgresql.GetBulkContentParams{ - SiteID: siteID, - Ids: contentIDs, - }) - if err != nil { - return nil, err - } - - items := make(map[string]ContentItem) - for _, content := range contents { - items[content.ID] = ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - HTMLContent: content.HtmlContent, - OriginalTemplate: getStringFromNullString(content.OriginalTemplate), - LastEditedBy: content.LastEditedBy, - } - } - return items, nil - - default: - return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) - } -} - -// GetAllContent retrieves all content items for a site -func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, error) { - switch c.database.GetDBType() { - case "sqlite3": - contents, err := c.database.GetSQLiteQueries().GetAllContent(context.Background(), siteID) - if err != nil { - return nil, err - } - - items := make(map[string]ContentItem) - for _, content := range contents { - items[content.ID] = ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - HTMLContent: content.HtmlContent, - OriginalTemplate: getStringFromNullString(content.OriginalTemplate), - LastEditedBy: content.LastEditedBy, - } - } - return items, nil - - case "postgresql": - contents, err := c.database.GetPostgreSQLQueries().GetAllContent(context.Background(), siteID) - if err != nil { - return nil, err - } - - items := make(map[string]ContentItem) - for _, content := range contents { - items[content.ID] = ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - HTMLContent: content.HtmlContent, - OriginalTemplate: getStringFromNullString(content.OriginalTemplate), - LastEditedBy: content.LastEditedBy, - } - } - return items, nil - - default: - return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) - } -} - -// CreateContent creates a new content item -func (c *DatabaseClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*ContentItem, error) { - switch c.database.GetDBType() { - case "sqlite3": - content, err := c.database.GetSQLiteQueries().CreateContent(context.Background(), sqlite.CreateContentParams{ - ID: contentID, - SiteID: siteID, - HtmlContent: htmlContent, - OriginalTemplate: ToNullString(originalTemplate), - LastEditedBy: lastEditedBy, - }) - if err != nil { - return nil, err - } - return &ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - HTMLContent: content.HtmlContent, - OriginalTemplate: getStringFromNullString(content.OriginalTemplate), - LastEditedBy: content.LastEditedBy, - }, nil - - case "postgresql": - content, err := c.database.GetPostgreSQLQueries().CreateContent(context.Background(), postgresql.CreateContentParams{ - ID: contentID, - SiteID: siteID, - HtmlContent: htmlContent, - OriginalTemplate: ToNullString(originalTemplate), - LastEditedBy: lastEditedBy, - }) - if err != nil { - return nil, err - } - return &ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - HTMLContent: content.HtmlContent, - OriginalTemplate: getStringFromNullString(content.OriginalTemplate), - LastEditedBy: content.LastEditedBy, - }, nil - - default: - return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) - } -} - - -// GetCollection retrieves a collection container -func (c *DatabaseClient) GetCollection(siteID, collectionID string) (*CollectionItem, error) { - switch c.database.GetDBType() { - case "sqlite3": - collection, err := c.database.GetSQLiteQueries().GetCollection(context.Background(), sqlite.GetCollectionParams{ - ID: collectionID, - SiteID: siteID, - }) - if err != nil { - return nil, err - } - return &CollectionItem{ - ID: collection.ID, - SiteID: collection.SiteID, - ContainerHTML: collection.ContainerHtml, - LastEditedBy: collection.LastEditedBy, - }, nil - - case "postgresql": - collection, err := c.database.GetPostgreSQLQueries().GetCollection(context.Background(), postgresql.GetCollectionParams{ - ID: collectionID, - SiteID: siteID, - }) - if err != nil { - return nil, err - } - return &CollectionItem{ - ID: collection.ID, - SiteID: collection.SiteID, - ContainerHTML: collection.ContainerHtml, - LastEditedBy: collection.LastEditedBy, - }, nil - - default: - return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) - } -} - -// CreateCollection creates a new collection container -func (c *DatabaseClient) CreateCollection(siteID, collectionID, containerHTML, lastEditedBy string) (*CollectionItem, error) { - switch c.database.GetDBType() { - case "sqlite3": - collection, err := c.database.GetSQLiteQueries().CreateCollection(context.Background(), sqlite.CreateCollectionParams{ - ID: collectionID, - SiteID: siteID, - ContainerHtml: containerHTML, - LastEditedBy: lastEditedBy, - }) - if err != nil { - return nil, err - } - return &CollectionItem{ - ID: collection.ID, - SiteID: collection.SiteID, - ContainerHTML: collection.ContainerHtml, - LastEditedBy: collection.LastEditedBy, - }, nil - - case "postgresql": - collection, err := c.database.GetPostgreSQLQueries().CreateCollection(context.Background(), postgresql.CreateCollectionParams{ - ID: collectionID, - SiteID: siteID, - ContainerHtml: containerHTML, - LastEditedBy: lastEditedBy, - }) - if err != nil { - return nil, err - } - return &CollectionItem{ - ID: collection.ID, - SiteID: collection.SiteID, - ContainerHTML: collection.ContainerHtml, - LastEditedBy: collection.LastEditedBy, - }, nil - - default: - return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) - } -} - -// GetCollectionItems retrieves all items in a collection with template information -func (c *DatabaseClient) GetCollectionItems(siteID, collectionID string) ([]CollectionItemWithTemplate, error) { - switch c.database.GetDBType() { - case "sqlite3": - items, err := c.database.GetSQLiteQueries().GetCollectionItemsWithTemplate(context.Background(), sqlite.GetCollectionItemsWithTemplateParams{ - CollectionID: collectionID, - SiteID: siteID, - }) - if err != nil { - return nil, err - } - - result := make([]CollectionItemWithTemplate, len(items)) - for i, item := range items { - result[i] = CollectionItemWithTemplate{ - ItemID: item.ItemID, - CollectionID: item.CollectionID, - SiteID: item.SiteID, - TemplateID: int(item.TemplateID), - HTMLContent: item.HtmlContent, - Position: int(item.Position), - LastEditedBy: item.LastEditedBy, - TemplateName: item.TemplateName, - HTMLTemplate: item.HtmlTemplate, - IsDefault: item.IsDefault != 0, // SQLite uses INTEGER for boolean - } - } - return result, nil - - case "postgresql": - items, err := c.database.GetPostgreSQLQueries().GetCollectionItemsWithTemplate(context.Background(), postgresql.GetCollectionItemsWithTemplateParams{ - CollectionID: collectionID, - SiteID: siteID, - }) - if err != nil { - return nil, err - } - - result := make([]CollectionItemWithTemplate, len(items)) - for i, item := range items { - result[i] = CollectionItemWithTemplate{ - ItemID: item.ItemID, - CollectionID: item.CollectionID, - SiteID: item.SiteID, - TemplateID: int(item.TemplateID), - HTMLContent: item.HtmlContent, - Position: int(item.Position), - LastEditedBy: item.LastEditedBy, - TemplateName: item.TemplateName, - HTMLTemplate: item.HtmlTemplate, - IsDefault: item.IsDefault, // PostgreSQL uses BOOLEAN - } - } - return result, nil - - default: - return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) - } -} - -// CreateCollectionTemplate creates a new template for a collection -func (c *DatabaseClient) CreateCollectionTemplate(siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error) { - switch c.database.GetDBType() { - case "sqlite3": - var isDefaultInt int64 - if isDefault { - isDefaultInt = 1 - } - - template, err := c.database.GetSQLiteQueries().CreateCollectionTemplate(context.Background(), sqlite.CreateCollectionTemplateParams{ - CollectionID: collectionID, - SiteID: siteID, - Name: name, - HtmlTemplate: htmlTemplate, - IsDefault: isDefaultInt, - }) - if err != nil { - return nil, err - } - return &CollectionTemplateItem{ - TemplateID: int(template.TemplateID), - CollectionID: template.CollectionID, - SiteID: template.SiteID, - Name: template.Name, - HTMLTemplate: template.HtmlTemplate, - IsDefault: template.IsDefault != 0, - }, nil - - case "postgresql": - template, err := c.database.GetPostgreSQLQueries().CreateCollectionTemplate(context.Background(), postgresql.CreateCollectionTemplateParams{ - CollectionID: collectionID, - SiteID: siteID, - Name: name, - HtmlTemplate: htmlTemplate, - IsDefault: isDefault, - }) - if err != nil { - return nil, err - } - return &CollectionTemplateItem{ - TemplateID: int(template.TemplateID), - CollectionID: template.CollectionID, - SiteID: template.SiteID, - Name: template.Name, - HTMLTemplate: template.HtmlTemplate, - IsDefault: template.IsDefault, - }, nil - - default: - return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) - } -} - -// GetCollectionTemplates retrieves all templates for a collection -func (c *DatabaseClient) GetCollectionTemplates(siteID, collectionID string) ([]CollectionTemplateItem, error) { - switch c.database.GetDBType() { - case "sqlite3": - templates, err := c.database.GetSQLiteQueries().GetCollectionTemplates(context.Background(), sqlite.GetCollectionTemplatesParams{ - CollectionID: collectionID, - SiteID: siteID, - }) - if err != nil { - return nil, err - } - - result := make([]CollectionTemplateItem, len(templates)) - for i, template := range templates { - result[i] = CollectionTemplateItem{ - TemplateID: int(template.TemplateID), - CollectionID: template.CollectionID, - SiteID: template.SiteID, - Name: template.Name, - HTMLTemplate: template.HtmlTemplate, - IsDefault: template.IsDefault != 0, // SQLite uses INTEGER for boolean - } - } - return result, nil - - case "postgresql": - templates, err := c.database.GetPostgreSQLQueries().GetCollectionTemplates(context.Background(), postgresql.GetCollectionTemplatesParams{ - CollectionID: collectionID, - SiteID: siteID, - }) - if err != nil { - return nil, err - } - - result := make([]CollectionTemplateItem, len(templates)) - for i, template := range templates { - result[i] = CollectionTemplateItem{ - TemplateID: int(template.TemplateID), - CollectionID: template.CollectionID, - SiteID: template.SiteID, - Name: template.Name, - HTMLTemplate: template.HtmlTemplate, - IsDefault: template.IsDefault, // PostgreSQL uses BOOLEAN - } - } - return result, nil - - default: - return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) - } -} - -// CreateCollectionItem creates a new collection item -func (c *DatabaseClient) CreateCollectionItem(siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) { - switch c.database.GetDBType() { - case "sqlite3": - item, err := c.database.GetSQLiteQueries().CreateCollectionItem(context.Background(), sqlite.CreateCollectionItemParams{ - ItemID: itemID, - CollectionID: collectionID, - SiteID: siteID, - TemplateID: int64(templateID), - HtmlContent: htmlContent, - Position: int64(position), - LastEditedBy: lastEditedBy, - }) - if err != nil { - return nil, err - } - return &CollectionItemWithTemplate{ - ItemID: item.ItemID, - CollectionID: item.CollectionID, - SiteID: item.SiteID, - TemplateID: int(item.TemplateID), - HTMLContent: item.HtmlContent, - Position: int(item.Position), - LastEditedBy: item.LastEditedBy, - }, nil - - case "postgresql": - item, err := c.database.GetPostgreSQLQueries().CreateCollectionItem(context.Background(), postgresql.CreateCollectionItemParams{ - ItemID: itemID, - CollectionID: collectionID, - SiteID: siteID, - TemplateID: int32(templateID), - HtmlContent: htmlContent, - Position: int32(position), - LastEditedBy: lastEditedBy, - }) - if err != nil { - return nil, err - } - return &CollectionItemWithTemplate{ - ItemID: item.ItemID, - CollectionID: item.CollectionID, - SiteID: item.SiteID, - TemplateID: int(item.TemplateID), - HTMLContent: item.HtmlContent, - Position: int(item.Position), - LastEditedBy: item.LastEditedBy, - }, nil - - default: - return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) - } -} - -// CreateCollectionItemAtomic creates a collection item with all its content entries atomically -func (c *DatabaseClient) CreateCollectionItemAtomic( - siteID, collectionID string, - templateID int, - lastEditedBy string, -) (*CollectionItemWithTemplate, error) { - // Get template HTML for processing - templates, err := c.GetCollectionTemplates(siteID, collectionID) - if err != nil { - return nil, fmt.Errorf("failed to get templates: %w", err) - } - - var templateHTML string - for _, template := range templates { - if template.TemplateID == templateID { - templateHTML = template.HTMLTemplate - break - } - } - - if templateHTML == "" { - return nil, fmt.Errorf("template %d not found", templateID) - } - - // Use unified engine approach (no more TemplateProcessor) - engine := NewContentEngine(c) - - // Create collection item using unified engine method - return engine.CreateCollectionItemFromTemplate( - siteID, collectionID, templateID, templateHTML, lastEditedBy, - ) -} diff --git a/internal/engine/engine.go b/internal/engine/engine.go index f649139..8809a7d 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -1,9 +1,11 @@ package engine import ( + "context" "fmt" "strings" + "github.com/insertr/insertr/internal/db" "golang.org/x/net/html" ) @@ -15,13 +17,13 @@ type AuthProvider struct { // ContentEngine is the unified content processing engine type ContentEngine struct { idGenerator *IDGenerator - client ContentClient + client db.ContentRepository authProvider *AuthProvider injector *Injector } // NewContentEngine creates a new content processing engine -func NewContentEngine(client ContentClient) *ContentEngine { +func NewContentEngine(client db.ContentRepository) *ContentEngine { authProvider := &AuthProvider{Type: "mock"} // default return &ContentEngine{ idGenerator: NewIDGenerator(), @@ -32,7 +34,7 @@ func NewContentEngine(client ContentClient) *ContentEngine { } // NewContentEngineWithAuth creates a new content processing engine with auth config -func NewContentEngineWithAuth(client ContentClient, authProvider *AuthProvider) *ContentEngine { +func NewContentEngineWithAuth(client db.ContentRepository, authProvider *AuthProvider) *ContentEngine { if authProvider == nil { authProvider = &AuthProvider{Type: "mock"} } @@ -64,7 +66,7 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro id := e.idGenerator.Generate(elem.Node, input.FilePath) // Database-first approach: Check if content already exists - existingContent, err := e.client.GetContent(input.SiteID, id) + existingContent, err := e.client.GetContent(context.Background(), input.SiteID, id) contentExists := (err == nil && existingContent != nil) generatedIDs[fmt.Sprintf("element_%d", i)] = id @@ -87,7 +89,7 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro originalTemplate := e.extractOriginalTemplate(elem.Node) // Store in database via content client - _, err := e.client.CreateContent(input.SiteID, id, htmlContent, originalTemplate, "system") + _, err := e.client.CreateContent(context.Background(), input.SiteID, id, htmlContent, originalTemplate, "system") if err != nil { // Log error but don't fail the enhancement - content just won't be stored fmt.Printf("โš ๏ธ Failed to store content for %s: %v\n", id, err) @@ -343,7 +345,7 @@ func (e *ContentEngine) injectContent(elements []ProcessedElement, siteID string elem := &elements[i] // Try to get content from database - contentItem, err := e.client.GetContent(siteID, elem.ID) + contentItem, err := e.client.GetContent(context.Background(), siteID, elem.ID) if err != nil { // Content not found is not an error - element just won't have injected content continue @@ -467,14 +469,14 @@ func (e *ContentEngine) getPlaceholderForElement(elementType string) string { // processCollection handles collection detection, persistence and reconstruction func (e *ContentEngine) processCollection(collectionNode *html.Node, collectionID, siteID string) error { // 1. Check if collection exists in database - existingCollection, err := e.client.GetCollection(siteID, collectionID) + existingCollection, err := e.client.GetCollection(context.Background(), siteID, collectionID) collectionExists := (err == nil && existingCollection != nil) if !collectionExists { // 2. New collection: extract container HTML and create collection record containerHTML := e.extractOriginalTemplate(collectionNode) - _, err := e.client.CreateCollection(siteID, collectionID, containerHTML, "system") + _, err := e.client.CreateCollection(context.Background(), siteID, collectionID, containerHTML, "system") if err != nil { return fmt.Errorf("failed to create collection %s: %w", collectionID, err) } @@ -494,7 +496,7 @@ func (e *ContentEngine) processCollection(collectionNode *html.Node, collectionI } // Get final item count for logging - existingItems, _ := e.client.GetCollectionItems(siteID, collectionID) + existingItems, _ := e.client.GetCollectionItems(context.Background(), siteID, collectionID) fmt.Printf("โœ… Reconstructed collection: %s from database (%d items)\n", collectionID, len(existingItems)) } @@ -515,7 +517,7 @@ func (e *ContentEngine) extractAndStoreTemplatesAndItems(collectionNode *html.No if len(templateElements) == 0 { // No existing children - create a default empty template - _, err := e.client.CreateCollectionTemplate(siteID, collectionID, "default", "
New item
", true) + _, err := e.client.CreateCollectionTemplate(context.Background(), siteID, collectionID, "default", "
New item
", true) if err != nil { return fmt.Errorf("failed to create default template: %w", err) } @@ -530,7 +532,7 @@ func (e *ContentEngine) extractAndStoreTemplatesAndItems(collectionNode *html.No templateName := fmt.Sprintf("template-%d", i+1) isDefault := (i == 0) // First template is default - template, err := e.client.CreateCollectionTemplate(siteID, collectionID, templateName, templateHTML, isDefault) + template, err := e.client.CreateCollectionTemplate(context.Background(), siteID, collectionID, templateName, templateHTML, isDefault) if err != nil { return fmt.Errorf("failed to create template %s: %w", templateName, err) } @@ -557,13 +559,13 @@ func (e *ContentEngine) extractAndStoreTemplatesAndItems(collectionNode *html.No // reconstructCollectionItems rebuilds collection items from database and adds them to DOM func (e *ContentEngine) reconstructCollectionItems(collectionNode *html.Node, collectionID, siteID string) error { // Get all items for this collection from database - items, err := e.client.GetCollectionItems(siteID, collectionID) + items, err := e.client.GetCollectionItems(context.Background(), siteID, collectionID) if err != nil { return fmt.Errorf("failed to get collection items: %w", err) } // Get templates for this collection - templates, err := e.client.GetCollectionTemplates(siteID, collectionID) + templates, err := e.client.GetCollectionTemplates(context.Background(), siteID, collectionID) if err != nil { return fmt.Errorf("failed to get collection templates: %w", err) } @@ -652,7 +654,7 @@ func (e *ContentEngine) processChildElementsAsContent(childElement *html.Node, s actualContent := ExtractTextContent(n) // Store as individual content entry (unified .insertr approach) - _, err := e.client.CreateContent(siteID, contentID, actualContent, "", "system") + _, err := e.client.CreateContent(context.Background(), siteID, contentID, actualContent, "", "system") if err != nil { fmt.Printf("โš ๏ธ Failed to create content %s: %v\n", contentID, err) return @@ -737,7 +739,7 @@ func (e *ContentEngine) CreateCollectionItemFromTemplate( templateID int, templateHTML string, lastEditedBy string, -) (*CollectionItemWithTemplate, error) { +) (*db.CollectionItemWithTemplate, error) { // Create virtual element from template (like enhancement path) virtualElement, err := e.createVirtualElementFromTemplate(templateHTML) if err != nil { @@ -763,7 +765,7 @@ func (e *ContentEngine) CreateCollectionItemFromTemplate( } // Create collection item with structural template - collectionItem, err := e.client.CreateCollectionItem( + collectionItem, err := e.client.CreateCollectionItem(context.Background(), siteID, collectionID, itemID, templateID, structuralTemplate, 0, lastEditedBy, ) if err != nil { @@ -838,7 +840,7 @@ func (e *ContentEngine) storeChildrenAsCollectionItems(collectionNode *html.Node templateID := templateIDs[i%len(templateIDs)] // Store structural template in collection_items (content lives in content table) - _, err = e.client.CreateCollectionItem(siteID, collectionID, itemID, templateID, structuralTemplate, i+1, "system") + _, err = e.client.CreateCollectionItem(context.Background(), siteID, collectionID, itemID, templateID, structuralTemplate, i+1, "system") if err != nil { return fmt.Errorf("failed to create collection item %s: %w", itemID, err) } diff --git a/internal/engine/injector.go b/internal/engine/injector.go index 07021cd..4a5fcff 100644 --- a/internal/engine/injector.go +++ b/internal/engine/injector.go @@ -1,22 +1,24 @@ package engine import ( + "context" "fmt" "log" "strings" + "github.com/insertr/insertr/internal/db" "golang.org/x/net/html" ) // Injector handles content injection into HTML elements type Injector struct { - client ContentClient + client db.ContentRepository siteID string authProvider *AuthProvider } // NewInjector creates a new content injector -func NewInjector(client ContentClient, siteID string) *Injector { +func NewInjector(client db.ContentRepository, siteID string) *Injector { return &Injector{ client: client, siteID: siteID, @@ -25,7 +27,7 @@ func NewInjector(client ContentClient, siteID string) *Injector { } // NewInjectorWithAuth creates a new content injector with auth provider -func NewInjectorWithAuth(client ContentClient, siteID string, authProvider *AuthProvider) *Injector { +func NewInjectorWithAuth(client db.ContentRepository, siteID string, authProvider *AuthProvider) *Injector { if authProvider == nil { authProvider = &AuthProvider{Type: "mock"} } @@ -39,7 +41,7 @@ func NewInjectorWithAuth(client ContentClient, siteID string, authProvider *Auth // InjectContent replaces element content with database values and adds content IDs func (i *Injector) InjectContent(element *Element, contentID string) error { // Fetch content from database/API - contentItem, err := i.client.GetContent(i.siteID, contentID) + contentItem, err := i.client.GetContent(context.Background(), i.siteID, contentID) if err != nil { return fmt.Errorf("fetching content for %s: %w", contentID, err) } @@ -68,7 +70,7 @@ func (i *Injector) InjectBulkContent(elements []ElementWithID) error { } // Bulk fetch content - contentMap, err := i.client.GetBulkContent(i.siteID, contentIDs) + contentMap, err := i.client.GetBulkContent(context.Background(), i.siteID, contentIDs) if err != nil { return fmt.Errorf("bulk fetching content: %w", err) } diff --git a/internal/engine/types.go b/internal/engine/types.go index d304555..7206c16 100644 --- a/internal/engine/types.go +++ b/internal/engine/types.go @@ -48,73 +48,3 @@ type ContentEntry struct { HTMLContent string Template string } - -// ContentClient interface for accessing content data -// This will be implemented by database clients, HTTP clients, and mock clients -type ContentClient interface { - GetContent(siteID, contentID string) (*ContentItem, error) - GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) - GetAllContent(siteID string) (map[string]ContentItem, error) - CreateContent(siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*ContentItem, error) - - // Collection operations - GetCollection(siteID, collectionID string) (*CollectionItem, error) - CreateCollection(siteID, collectionID, containerHTML, lastEditedBy string) (*CollectionItem, error) - GetCollectionItems(siteID, collectionID string) ([]CollectionItemWithTemplate, error) - GetCollectionTemplates(siteID, collectionID string) ([]CollectionTemplateItem, error) - CreateCollectionTemplate(siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error) - CreateCollectionItem(siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) - CreateCollectionItemAtomic(siteID, collectionID string, templateID int, lastEditedBy string) (*CollectionItemWithTemplate, error) -} - -// ContentItem represents a piece of content from the database -type ContentItem struct { - ID string `json:"id"` - SiteID string `json:"site_id"` - HTMLContent string `json:"html_content"` - OriginalTemplate string `json:"original_template"` - UpdatedAt string `json:"updated_at"` - LastEditedBy string `json:"last_edited_by,omitempty"` -} - -// ContentResponse represents the API response structure -type ContentResponse struct { - Content []ContentItem `json:"content"` - Error string `json:"error,omitempty"` -} - -// CollectionItem represents a collection container from the database -type CollectionItem struct { - ID string `json:"id"` - SiteID string `json:"site_id"` - ContainerHTML string `json:"container_html"` - UpdatedAt string `json:"updated_at"` - LastEditedBy string `json:"last_edited_by,omitempty"` -} - -// CollectionTemplateItem represents a collection template from the database -type CollectionTemplateItem struct { - TemplateID int `json:"template_id"` - CollectionID string `json:"collection_id"` - SiteID string `json:"site_id"` - Name string `json:"name"` - HTMLTemplate string `json:"html_template"` - IsDefault bool `json:"is_default"` -} - -// CollectionItemWithTemplate represents a collection item with its template information -type CollectionItemWithTemplate struct { - ItemID string `json:"item_id"` - CollectionID string `json:"collection_id"` - SiteID string `json:"site_id"` - TemplateID int `json:"template_id"` - HTMLContent string `json:"html_content"` - Position int `json:"position"` - UpdatedAt string `json:"updated_at"` - LastEditedBy string `json:"last_edited_by"` - - // Template information - TemplateName string `json:"template_name"` - HTMLTemplate string `json:"html_template"` - IsDefault bool `json:"is_default"` -} diff --git a/internal/engine/utils.go b/internal/engine/utils.go index 777c7d4..0dcdb9a 100644 --- a/internal/engine/utils.go +++ b/internal/engine/utils.go @@ -1,7 +1,6 @@ package engine import ( - "database/sql" "strings" "golang.org/x/net/html" @@ -38,7 +37,6 @@ func getAttribute(node *html.Node, key string) string { return "" } - // hasOnlyTextContent checks if a node contains only text content (no nested HTML elements) // DEPRECATED: Use hasEditableContent for more sophisticated detection func hasOnlyTextContent(node *html.Node) bool { @@ -303,7 +301,6 @@ func hasInsertrClass(node *html.Node) bool { return false } - // isSelfClosing checks if an element is typically self-closing func isSelfClosing(node *html.Node) bool { if node.Type != html.ElementNode { @@ -376,24 +373,6 @@ func FindViableChildren(node *html.Node) []*html.Node { return findViableChildren(node) } -// SQL utility functions for consistent null string handling - -// ToNullString converts a string to sql.NullString -func ToNullString(s string) sql.NullString { - if s == "" { - return sql.NullString{Valid: false} - } - return sql.NullString{String: s, Valid: true} -} - -// FromNullString converts sql.NullString to string -func FromNullString(ns sql.NullString) string { - if ns.Valid { - return ns.String - } - return "" -} - // Text extraction utility functions // ExtractTextContent extracts all text content from an HTML node recursively