feat: implement unified content engine to eliminate ID generation inconsistencies
- Create internal/engine module as single source of truth for content processing - Consolidate 4 separate ID generation systems into one unified engine - Update API handlers to use engine for consistent server-side ID generation - Remove frontend client-side ID generation, delegate to server engine - Ensure identical HTML markup + file path produces identical content IDs - Resolve content persistence failures caused by ID fragmentation between manual editing and enhancement processes
This commit is contained in:
@@ -19,8 +19,7 @@ import (
|
||||
"github.com/insertr/insertr/internal/db"
|
||||
"github.com/insertr/insertr/internal/db/postgresql"
|
||||
"github.com/insertr/insertr/internal/db/sqlite"
|
||||
"github.com/insertr/insertr/internal/parser"
|
||||
"golang.org/x/net/html"
|
||||
"github.com/insertr/insertr/internal/engine"
|
||||
)
|
||||
|
||||
// ContentHandler handles all content-related HTTP requests
|
||||
@@ -28,14 +27,19 @@ type ContentHandler struct {
|
||||
database *db.Database
|
||||
authService *auth.AuthService
|
||||
siteManager *content.SiteManager
|
||||
engine *engine.ContentEngine
|
||||
}
|
||||
|
||||
// NewContentHandler creates a new content handler
|
||||
func NewContentHandler(database *db.Database, authService *auth.AuthService) *ContentHandler {
|
||||
// Create database client for engine
|
||||
dbClient := engine.NewDatabaseClient(database)
|
||||
|
||||
return &ContentHandler{
|
||||
database: database,
|
||||
authService: authService,
|
||||
siteManager: nil, // Will be set via SetSiteManager
|
||||
engine: engine.NewContentEngine(dbClient),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,16 +240,31 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
|
||||
siteID = "default" // final fallback
|
||||
}
|
||||
|
||||
// Determine content ID - use provided ID or generate from element context
|
||||
contentID := req.ID
|
||||
if contentID == "" {
|
||||
if req.ElementContext == nil {
|
||||
http.Error(w, "Either ID or element_context required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
contentID = h.generateContentID(req.ElementContext)
|
||||
// Generate content ID using the unified engine
|
||||
if req.HTMLMarkup == "" {
|
||||
http.Error(w, "html_markup is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
result, engineErr := h.engine.ProcessContent(engine.ContentInput{
|
||||
HTML: []byte(req.HTMLMarkup),
|
||||
FilePath: req.FilePath,
|
||||
SiteID: siteID,
|
||||
Mode: engine.IDGeneration,
|
||||
})
|
||||
if engineErr != nil {
|
||||
http.Error(w, fmt.Sprintf("ID generation failed: %v", engineErr), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(result.Elements) == 0 {
|
||||
http.Error(w, "No insertr elements found in HTML markup", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Use the ID generated by the engine for the first element
|
||||
contentID := result.Elements[0].ID
|
||||
|
||||
// Extract user from request using authentication service
|
||||
userInfo, authErr := h.authService.ExtractUserFromRequest(r)
|
||||
if authErr != nil {
|
||||
@@ -681,44 +700,7 @@ func (h *ContentHandler) versionMatches(version interface{}, contentID, siteID s
|
||||
return false
|
||||
}
|
||||
|
||||
// generateContentID creates a content ID from element context using the parser
|
||||
func (h *ContentHandler) generateContentID(ctx *ElementContext) string {
|
||||
// Create virtual node for existing parser ID generation
|
||||
virtualNode := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: ctx.Tag,
|
||||
Attr: []html.Attribute{
|
||||
{Key: "class", Val: strings.Join(ctx.Classes, " ")},
|
||||
},
|
||||
}
|
||||
|
||||
// Add parent context as a virtual parent node if provided
|
||||
if ctx.ParentContext != "" && ctx.ParentContext != "content" {
|
||||
parentNode := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: "section",
|
||||
Attr: []html.Attribute{
|
||||
{Key: "class", Val: ctx.ParentContext},
|
||||
},
|
||||
}
|
||||
parentNode.AppendChild(virtualNode)
|
||||
virtualNode.Parent = parentNode
|
||||
}
|
||||
|
||||
// Add text content for hash generation
|
||||
if ctx.OriginalContent != "" {
|
||||
textNode := &html.Node{
|
||||
Type: html.TextNode,
|
||||
Data: ctx.OriginalContent,
|
||||
}
|
||||
virtualNode.AppendChild(textNode)
|
||||
}
|
||||
|
||||
// Use existing parser ID generator
|
||||
// For API-generated IDs, use a placeholder filePath since we don't have file context
|
||||
idGenerator := parser.NewIDGenerator()
|
||||
return idGenerator.Generate(virtualNode, "api-generated")
|
||||
}
|
||||
// generateContentID function removed - using unified ContentEngine instead
|
||||
|
||||
// ServeInsertrJS handles GET /insertr.js - serves the insertr JavaScript library
|
||||
func (h *ContentHandler) ServeInsertrJS(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -42,12 +42,12 @@ type ElementContext struct {
|
||||
|
||||
// Request models
|
||||
type CreateContentRequest struct {
|
||||
ID string `json:"id,omitempty"` // For enhanced sites
|
||||
ElementContext *ElementContext `json:"element_context,omitempty"` // For non-enhanced sites
|
||||
SiteID string `json:"site_id,omitempty"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
HTMLMarkup string `json:"html_markup"` // HTML markup of the element
|
||||
FilePath string `json:"file_path"` // File path for consistent ID generation
|
||||
Value string `json:"value"` // Content value
|
||||
Type string `json:"type"` // Content type
|
||||
SiteID string `json:"site_id,omitempty"` // Site identifier
|
||||
CreatedBy string `json:"created_by,omitempty"` // User who created the content
|
||||
}
|
||||
|
||||
type RollbackContentRequest struct {
|
||||
|
||||
Reference in New Issue
Block a user