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
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
285
internal/db/postgresql_repository.go
Normal file
285
internal/db/postgresql_repository.go
Normal file
@@ -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()
|
||||
}
|
||||
103
internal/db/repository.go
Normal file
103
internal/db/repository.go
Normal file
@@ -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 ""
|
||||
}
|
||||
290
internal/db/sqlite_repository.go
Normal file
290
internal/db/sqlite_repository.go
Normal file
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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", "<div>New item</div>", true)
|
||||
_, err := e.client.CreateCollectionTemplate(context.Background(), siteID, collectionID, "default", "<div>New item</div>", 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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user