feat: Complete HTML-first architecture implementation with API integration
- Replace value field with html_content for direct HTML storage - Add original_template field for style detection preservation - Remove all markdown processing from injector (delete markdown.go) - Fix critical content extraction/injection bugs in engine - Add missing UpdateContent PUT handler for content persistence - Fix API client field names and add updateContent() method - Resolve content type validation (only allow text/link types) - Add UUID-based ID generation to prevent collisions - Complete first-pass processing workflow for unprocessed elements - Verify end-to-end: Enhancement → Database → API → Editor → Persistence All 37 files updated for HTML-first content management system. Phase 3a implementation complete and production ready.
This commit is contained in:
@@ -22,6 +22,21 @@ import (
|
||||
"github.com/insertr/insertr/internal/engine"
|
||||
)
|
||||
|
||||
// Helper functions for sql.NullString conversion
|
||||
func toNullString(s string) sql.NullString {
|
||||
if s == "" {
|
||||
return sql.NullString{Valid: false}
|
||||
}
|
||||
return sql.NullString{String: s, Valid: true}
|
||||
}
|
||||
|
||||
func fromNullString(ns sql.NullString) string {
|
||||
if ns.Valid {
|
||||
return ns.String
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ContentHandler handles all content-related HTTP requests
|
||||
type ContentHandler struct {
|
||||
database *db.Database
|
||||
@@ -314,19 +329,21 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
|
||||
switch h.database.GetDBType() {
|
||||
case "sqlite3":
|
||||
content, err = h.database.GetSQLiteQueries().UpsertContent(context.Background(), sqlite.UpsertContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
Value: req.Value,
|
||||
Type: contentType,
|
||||
LastEditedBy: userID,
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
HtmlContent: req.HTMLContent,
|
||||
OriginalTemplate: toNullString(req.OriginalTemplate),
|
||||
Type: contentType,
|
||||
LastEditedBy: userID,
|
||||
})
|
||||
case "postgresql":
|
||||
content, err = h.database.GetPostgreSQLQueries().UpsertContent(context.Background(), postgresql.UpsertContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
Value: req.Value,
|
||||
Type: contentType,
|
||||
LastEditedBy: userID,
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
HtmlContent: req.HTMLContent,
|
||||
OriginalTemplate: toNullString(req.OriginalTemplate),
|
||||
Type: contentType,
|
||||
LastEditedBy: userID,
|
||||
})
|
||||
default:
|
||||
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
|
||||
@@ -363,6 +380,111 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(item)
|
||||
}
|
||||
|
||||
// UpdateContent handles PUT /api/content/{id}
|
||||
func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
|
||||
contentID := chi.URLParam(r, "id")
|
||||
siteID := r.URL.Query().Get("site_id")
|
||||
|
||||
if siteID == "" {
|
||||
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
HTMLContent string `json:"html_content"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract user from request using authentication service
|
||||
userInfo, authErr := h.authService.ExtractUserFromRequest(r)
|
||||
if authErr != nil {
|
||||
http.Error(w, fmt.Sprintf("Authentication error: %v", authErr), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
userID := userInfo.ID
|
||||
|
||||
// Get existing content for version history
|
||||
var existingContent interface{}
|
||||
var err error
|
||||
|
||||
switch h.database.GetDBType() {
|
||||
case "sqlite3":
|
||||
existingContent, err = h.database.GetSQLiteQueries().GetContent(context.Background(), sqlite.GetContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
})
|
||||
case "postgresql":
|
||||
existingContent, err = h.database.GetPostgreSQLQueries().GetContent(context.Background(), postgresql.GetContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
})
|
||||
default:
|
||||
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Content not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Archive existing version before update
|
||||
if err := h.createContentVersion(existingContent); err != nil {
|
||||
fmt.Printf("Warning: Failed to create content version: %v\n", err)
|
||||
}
|
||||
|
||||
// Update content
|
||||
var updatedContent interface{}
|
||||
|
||||
switch h.database.GetDBType() {
|
||||
case "sqlite3":
|
||||
updatedContent, err = h.database.GetSQLiteQueries().UpdateContent(context.Background(), sqlite.UpdateContentParams{
|
||||
HtmlContent: req.HTMLContent,
|
||||
Type: h.getContentType(existingContent),
|
||||
LastEditedBy: userID,
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
})
|
||||
case "postgresql":
|
||||
updatedContent, err = h.database.GetPostgreSQLQueries().UpdateContent(context.Background(), postgresql.UpdateContentParams{
|
||||
HtmlContent: req.HTMLContent,
|
||||
Type: h.getContentType(existingContent),
|
||||
LastEditedBy: userID,
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
})
|
||||
default:
|
||||
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to update content: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
item := h.convertToAPIContent(updatedContent)
|
||||
|
||||
// Trigger file enhancement if site is registered for auto-enhancement
|
||||
if h.siteManager != nil && h.siteManager.IsAutoEnhanceEnabled(siteID) {
|
||||
go func() {
|
||||
if err := h.siteManager.EnhanceSite(siteID); err != nil {
|
||||
log.Printf("⚠️ Failed to enhance site %s: %v", siteID, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(item)
|
||||
}
|
||||
|
||||
// DeleteContent handles DELETE /api/content/{id}
|
||||
func (h *ContentHandler) DeleteContent(w http.ResponseWriter, r *http.Request) {
|
||||
contentID := chi.URLParam(r, "id")
|
||||
@@ -541,7 +663,7 @@ func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request)
|
||||
case "sqlite3":
|
||||
sqliteVersion := targetVersion.(sqlite.ContentVersion)
|
||||
updatedContent, err = h.database.GetSQLiteQueries().UpdateContent(context.Background(), sqlite.UpdateContentParams{
|
||||
Value: sqliteVersion.Value,
|
||||
HtmlContent: sqliteVersion.HtmlContent,
|
||||
Type: sqliteVersion.Type,
|
||||
LastEditedBy: userID,
|
||||
ID: contentID,
|
||||
@@ -550,7 +672,7 @@ func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request)
|
||||
case "postgresql":
|
||||
pgVersion := targetVersion.(postgresql.ContentVersion)
|
||||
updatedContent, err = h.database.GetPostgreSQLQueries().UpdateContent(context.Background(), postgresql.UpdateContentParams{
|
||||
Value: pgVersion.Value,
|
||||
HtmlContent: pgVersion.HtmlContent,
|
||||
Type: pgVersion.Type,
|
||||
LastEditedBy: userID,
|
||||
ID: contentID,
|
||||
@@ -578,24 +700,26 @@ func (h *ContentHandler) convertToAPIContent(content interface{}) ContentItem {
|
||||
case "sqlite3":
|
||||
c := content.(sqlite.Content)
|
||||
return ContentItem{
|
||||
ID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
Value: c.Value,
|
||||
Type: c.Type,
|
||||
CreatedAt: time.Unix(c.CreatedAt, 0),
|
||||
UpdatedAt: time.Unix(c.UpdatedAt, 0),
|
||||
LastEditedBy: c.LastEditedBy,
|
||||
ID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
HTMLContent: c.HtmlContent,
|
||||
OriginalTemplate: fromNullString(c.OriginalTemplate),
|
||||
Type: c.Type,
|
||||
CreatedAt: time.Unix(c.CreatedAt, 0),
|
||||
UpdatedAt: time.Unix(c.UpdatedAt, 0),
|
||||
LastEditedBy: c.LastEditedBy,
|
||||
}
|
||||
case "postgresql":
|
||||
c := content.(postgresql.Content)
|
||||
return ContentItem{
|
||||
ID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
Value: c.Value,
|
||||
Type: c.Type,
|
||||
CreatedAt: time.Unix(c.CreatedAt, 0),
|
||||
UpdatedAt: time.Unix(c.UpdatedAt, 0),
|
||||
LastEditedBy: c.LastEditedBy,
|
||||
ID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
HTMLContent: c.HtmlContent,
|
||||
OriginalTemplate: fromNullString(c.OriginalTemplate),
|
||||
Type: c.Type,
|
||||
CreatedAt: time.Unix(c.CreatedAt, 0),
|
||||
UpdatedAt: time.Unix(c.UpdatedAt, 0),
|
||||
LastEditedBy: c.LastEditedBy,
|
||||
}
|
||||
}
|
||||
return ContentItem{} // Should never happen
|
||||
@@ -628,13 +752,14 @@ func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []Cont
|
||||
versions := make([]ContentVersion, len(list))
|
||||
for i, version := range list {
|
||||
versions[i] = ContentVersion{
|
||||
VersionID: version.VersionID,
|
||||
ContentID: version.ContentID,
|
||||
SiteID: version.SiteID,
|
||||
Value: version.Value,
|
||||
Type: version.Type,
|
||||
CreatedAt: time.Unix(version.CreatedAt, 0),
|
||||
CreatedBy: version.CreatedBy,
|
||||
VersionID: version.VersionID,
|
||||
ContentID: version.ContentID,
|
||||
SiteID: version.SiteID,
|
||||
HTMLContent: version.HtmlContent,
|
||||
OriginalTemplate: fromNullString(version.OriginalTemplate),
|
||||
Type: version.Type,
|
||||
CreatedAt: time.Unix(version.CreatedAt, 0),
|
||||
CreatedBy: version.CreatedBy,
|
||||
}
|
||||
}
|
||||
return versions
|
||||
@@ -643,13 +768,14 @@ func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []Cont
|
||||
versions := make([]ContentVersion, len(list))
|
||||
for i, version := range list {
|
||||
versions[i] = ContentVersion{
|
||||
VersionID: int64(version.VersionID),
|
||||
ContentID: version.ContentID,
|
||||
SiteID: version.SiteID,
|
||||
Value: version.Value,
|
||||
Type: version.Type,
|
||||
CreatedAt: time.Unix(version.CreatedAt, 0),
|
||||
CreatedBy: version.CreatedBy,
|
||||
VersionID: int64(version.VersionID),
|
||||
ContentID: version.ContentID,
|
||||
SiteID: version.SiteID,
|
||||
HTMLContent: version.HtmlContent,
|
||||
OriginalTemplate: fromNullString(version.OriginalTemplate),
|
||||
Type: version.Type,
|
||||
CreatedAt: time.Unix(version.CreatedAt, 0),
|
||||
CreatedBy: version.CreatedBy,
|
||||
}
|
||||
}
|
||||
return versions
|
||||
@@ -662,20 +788,22 @@ func (h *ContentHandler) createContentVersion(content interface{}) error {
|
||||
case "sqlite3":
|
||||
c := content.(sqlite.Content)
|
||||
return h.database.GetSQLiteQueries().CreateContentVersion(context.Background(), sqlite.CreateContentVersionParams{
|
||||
ContentID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
Value: c.Value,
|
||||
Type: c.Type,
|
||||
CreatedBy: c.LastEditedBy,
|
||||
ContentID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
HtmlContent: c.HtmlContent,
|
||||
OriginalTemplate: c.OriginalTemplate,
|
||||
Type: c.Type,
|
||||
CreatedBy: c.LastEditedBy,
|
||||
})
|
||||
case "postgresql":
|
||||
c := content.(postgresql.Content)
|
||||
return h.database.GetPostgreSQLQueries().CreateContentVersion(context.Background(), postgresql.CreateContentVersionParams{
|
||||
ContentID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
Value: c.Value,
|
||||
Type: c.Type,
|
||||
CreatedBy: c.LastEditedBy,
|
||||
ContentID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
HtmlContent: c.HtmlContent,
|
||||
OriginalTemplate: c.OriginalTemplate,
|
||||
Type: c.Type,
|
||||
CreatedBy: c.LastEditedBy,
|
||||
})
|
||||
}
|
||||
return fmt.Errorf("unsupported database type")
|
||||
|
||||
Reference in New Issue
Block a user