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:
2025-09-20 16:42:00 +02:00
parent bb5ea6f873
commit 2177055c76
37 changed files with 1189 additions and 737 deletions

View File

@@ -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")

View File

@@ -4,23 +4,25 @@ import "time"
// API request/response models
type ContentItem struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"`
SiteID string `json:"site_id"`
HTMLContent string `json:"html_content"`
OriginalTemplate string `json:"original_template"`
Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
}
type ContentVersion struct {
VersionID int64 `json:"version_id"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by"`
VersionID int64 `json:"version_id"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
HTMLContent string `json:"html_content"`
OriginalTemplate string `json:"original_template"`
Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by"`
}
type ContentResponse struct {
@@ -42,12 +44,13 @@ type ElementContext struct {
// Request models
type CreateContentRequest struct {
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
HTMLMarkup string `json:"html_markup"` // HTML markup of the element
FilePath string `json:"file_path"` // File path for consistent ID generation
HTMLContent string `json:"html_content"` // HTML content value
OriginalTemplate string `json:"original_template"` // Original template markup
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 {

View File

@@ -164,3 +164,10 @@ func (c *HTTPClient) GetAllContent(siteID string) (map[string]engine.ContentItem
return result, nil
}
// CreateContent creates a new content item via HTTP API
func (c *HTTPClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, contentType, lastEditedBy string) (*engine.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")
}

View File

@@ -12,6 +12,14 @@ import (
"github.com/insertr/insertr/internal/engine"
)
// Helper function to convert sql.NullString to string
func getStringFromNullString(ns sql.NullString) string {
if ns.Valid {
return ns.String
}
return ""
}
// DatabaseClient implements ContentClient for direct database access
type DatabaseClient struct {
db *db.Database
@@ -132,20 +140,22 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) engine.Conten
case "sqlite3":
c := content.(sqlite.Content)
return engine.ContentItem{
ID: c.ID,
SiteID: c.SiteID,
Value: c.Value,
Type: c.Type,
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
ID: c.ID,
SiteID: c.SiteID,
HTMLContent: c.HtmlContent,
OriginalTemplate: getStringFromNullString(c.OriginalTemplate),
Type: c.Type,
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
}
case "postgresql":
c := content.(postgresql.Content)
return engine.ContentItem{
ID: c.ID,
SiteID: c.SiteID,
Value: c.Value,
Type: c.Type,
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
ID: c.ID,
SiteID: c.SiteID,
HTMLContent: c.HtmlContent,
OriginalTemplate: getStringFromNullString(c.OriginalTemplate),
Type: c.Type,
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
}
}
return engine.ContentItem{} // Should never happen
@@ -171,3 +181,61 @@ func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []eng
}
return []engine.ContentItem{} // Should never happen
}
// CreateContent creates a new content item
func (c *DatabaseClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, contentType, lastEditedBy string) (*engine.ContentItem, error) {
switch c.db.GetDBType() {
case "sqlite3":
content, err := c.db.GetSQLiteQueries().CreateContent(context.Background(), sqlite.CreateContentParams{
ID: contentID,
SiteID: siteID,
HtmlContent: htmlContent,
OriginalTemplate: toNullString(originalTemplate),
Type: contentType,
LastEditedBy: lastEditedBy,
})
if err != nil {
return nil, err
}
return &engine.ContentItem{
ID: content.ID,
SiteID: content.SiteID,
HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type,
LastEditedBy: content.LastEditedBy,
}, nil
case "postgresql":
content, err := c.db.GetPostgreSQLQueries().CreateContent(context.Background(), postgresql.CreateContentParams{
ID: contentID,
SiteID: siteID,
HtmlContent: htmlContent,
OriginalTemplate: toNullString(originalTemplate),
Type: contentType,
LastEditedBy: lastEditedBy,
})
if err != nil {
return nil, err
}
return &engine.ContentItem{
ID: content.ID,
SiteID: content.SiteID,
HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type,
LastEditedBy: content.LastEditedBy,
}, nil
default:
return nil, fmt.Errorf("unsupported database type: %s", c.db.GetDBType())
}
}
// Helper function to convert string to sql.NullString
func toNullString(s string) sql.NullString {
if s == "" {
return sql.NullString{Valid: false}
}
return sql.NullString{String: s, Valid: true}
}

View File

@@ -17,82 +17,82 @@ func NewMockClient() *MockClient {
data := map[string]engine.ContentItem{
// Navigation (index.html has collision suffix)
"navbar-logo-2b10ad": {
ID: "navbar-logo-2b10ad",
SiteID: "demo",
Value: "Acme Consulting Solutions",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
ID: "navbar-logo-2b10ad",
SiteID: "demo",
HTMLContent: "Acme Consulting Solutions",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
"navbar-logo-2b10ad-a44bad": {
ID: "navbar-logo-2b10ad-a44bad",
SiteID: "demo",
Value: "Acme Business Advisors",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
ID: "navbar-logo-2b10ad-a44bad",
SiteID: "demo",
HTMLContent: "Acme Business Advisors",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
// Hero Section - index.html (updated with actual IDs)
"hero-title-7cfeea": {
ID: "hero-title-7cfeea",
SiteID: "demo",
Value: "Transform Your Business with Strategic Expertise",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
ID: "hero-title-7cfeea",
SiteID: "demo",
HTMLContent: "Transform Your Business with Strategic Expertise",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
"hero-lead-e47475": {
ID: "hero-lead-e47475",
SiteID: "demo",
Value: "We help **ambitious businesses** grow through strategic planning, process optimization, and digital transformation. Our team brings 20+ years of experience to accelerate your success.",
Type: "markdown",
UpdatedAt: time.Now().Format(time.RFC3339),
ID: "hero-lead-e47475",
SiteID: "demo",
HTMLContent: "We help <strong>ambitious businesses</strong> grow through strategic planning, process optimization, and digital transformation. Our team brings 20+ years of experience to accelerate your success.",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
"hero-link-76c620": {
ID: "hero-link-76c620",
SiteID: "demo",
Value: "Schedule Free Consultation",
Type: "link",
UpdatedAt: time.Now().Format(time.RFC3339),
ID: "hero-link-76c620",
SiteID: "demo",
HTMLContent: "Schedule Free Consultation",
Type: "link",
UpdatedAt: time.Now().Format(time.RFC3339),
},
// Hero Section - about.html
"hero-title-c70343": {
ID: "hero-title-c70343",
SiteID: "demo",
Value: "About Our Consulting Expertise",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
ID: "hero-title-c70343",
SiteID: "demo",
HTMLContent: "About Our Consulting Expertise",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
"hero-lead-673026": {
ID: "hero-lead-673026",
SiteID: "demo",
Value: "We're a team of **experienced consultants** dedicated to helping small businesses thrive in today's competitive marketplace through proven strategies.",
Type: "markdown",
UpdatedAt: time.Now().Format(time.RFC3339),
ID: "hero-lead-673026",
SiteID: "demo",
HTMLContent: "We're a team of <strong>experienced consultants</strong> dedicated to helping small businesses thrive in today's competitive marketplace through proven strategies.",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
// Services Section
"services-subtitle-c8927c": {
ID: "services-subtitle-c8927c",
SiteID: "demo",
Value: "Our Story",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
ID: "services-subtitle-c8927c",
SiteID: "demo",
HTMLContent: "Our Story",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
"services-text-0d96da": {
ID: "services-text-0d96da",
SiteID: "demo",
Value: "**Founded in 2020**, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.",
Type: "markdown",
UpdatedAt: time.Now().Format(time.RFC3339),
ID: "services-text-0d96da",
SiteID: "demo",
HTMLContent: "<strong>Founded in 2020</strong>, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
// Default fallback for any missing content
"default": {
ID: "default",
SiteID: "demo",
Value: "[Enhanced Content]",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
ID: "default",
SiteID: "demo",
HTMLContent: "[Enhanced Content]",
Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339),
},
}
@@ -138,3 +138,22 @@ func (m *MockClient) GetAllContent(siteID string) (map[string]engine.ContentItem
return result, nil
}
// CreateContent creates a new mock content item
func (m *MockClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, contentType, lastEditedBy string) (*engine.ContentItem, error) {
// For mock client, just create and store the item
item := engine.ContentItem{
ID: contentID,
SiteID: siteID,
HTMLContent: htmlContent,
OriginalTemplate: originalTemplate,
Type: contentType,
UpdatedAt: time.Now().Format(time.RFC3339),
LastEditedBy: lastEditedBy,
}
// Store in mock data
m.data[contentID] = item
return &item, nil
}

View File

@@ -1,34 +1,37 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
// source: content.sql
package postgresql
import (
"context"
"database/sql"
"strings"
)
const createContent = `-- name: CreateContent :one
INSERT INTO content (id, site_id, value, type, last_edited_by)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by
INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
`
type CreateContentParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"`
}
func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (Content, error) {
row := q.db.QueryRowContext(ctx, createContent,
arg.ID,
arg.SiteID,
arg.Value,
arg.HtmlContent,
arg.OriginalTemplate,
arg.Type,
arg.LastEditedBy,
)
@@ -36,7 +39,8 @@ func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (C
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
@@ -45,6 +49,16 @@ func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (C
return i, err
}
const deleteAllSiteContent = `-- name: DeleteAllSiteContent :exec
DELETE FROM content
WHERE site_id = $1
`
func (q *Queries) DeleteAllSiteContent(ctx context.Context, siteID string) error {
_, err := q.db.ExecContext(ctx, deleteAllSiteContent, siteID)
return err
}
const deleteContent = `-- name: DeleteContent :exec
DELETE FROM content
WHERE id = $1 AND site_id = $2
@@ -61,7 +75,7 @@ func (q *Queries) DeleteContent(ctx context.Context, arg DeleteContentParams) er
}
const getAllContent = `-- name: GetAllContent :many
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
FROM content
WHERE site_id = $1
ORDER BY updated_at DESC
@@ -79,7 +93,8 @@ func (q *Queries) GetAllContent(ctx context.Context, siteID string) ([]Content,
if err := rows.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
@@ -99,7 +114,7 @@ func (q *Queries) GetAllContent(ctx context.Context, siteID string) ([]Content,
}
const getBulkContent = `-- name: GetBulkContent :many
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
FROM content
WHERE site_id = $1 AND id IN ($2)
`
@@ -132,7 +147,8 @@ func (q *Queries) GetBulkContent(ctx context.Context, arg GetBulkContentParams)
if err := rows.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
@@ -152,7 +168,7 @@ func (q *Queries) GetBulkContent(ctx context.Context, arg GetBulkContentParams)
}
const getContent = `-- name: GetContent :one
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
FROM content
WHERE id = $1 AND site_id = $2
`
@@ -168,7 +184,8 @@ func (q *Queries) GetContent(ctx context.Context, arg GetContentParams) (Content
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
@@ -179,13 +196,13 @@ func (q *Queries) GetContent(ctx context.Context, arg GetContentParams) (Content
const updateContent = `-- name: UpdateContent :one
UPDATE content
SET value = $1, type = $2, last_edited_by = $3
SET html_content = $1, type = $2, last_edited_by = $3
WHERE id = $4 AND site_id = $5
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by
RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
`
type UpdateContentParams struct {
Value string `json:"value"`
HtmlContent string `json:"html_content"`
Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"`
@@ -194,7 +211,7 @@ type UpdateContentParams struct {
func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error) {
row := q.db.QueryRowContext(ctx, updateContent,
arg.Value,
arg.HtmlContent,
arg.Type,
arg.LastEditedBy,
arg.ID,
@@ -204,7 +221,8 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
@@ -214,28 +232,30 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C
}
const upsertContent = `-- name: UpsertContent :one
INSERT INTO content (id, site_id, value, type, last_edited_by)
VALUES ($1, $2, $3, $4, $5)
INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT(id, site_id) DO UPDATE SET
value = EXCLUDED.value,
html_content = EXCLUDED.html_content,
type = EXCLUDED.type,
last_edited_by = EXCLUDED.last_edited_by
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by
RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
`
type UpsertContentParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"`
}
func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) {
row := q.db.QueryRowContext(ctx, upsertContent,
arg.ID,
arg.SiteID,
arg.Value,
arg.HtmlContent,
arg.OriginalTemplate,
arg.Type,
arg.LastEditedBy,
)
@@ -243,7 +263,8 @@ func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (C
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
package postgresql

View File

@@ -1,25 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
package postgresql
import (
"database/sql"
)
type Content struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
}
type ContentVersion struct {
VersionID int32 `json:"version_id"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
VersionID int32 `json:"version_id"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
}

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
package postgresql
@@ -15,6 +15,7 @@ type Querier interface {
CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error
CreateUpdateFunction(ctx context.Context) error
CreateVersionsLookupIndex(ctx context.Context) error
DeleteAllSiteContent(ctx context.Context, siteID string) error
DeleteContent(ctx context.Context, arg DeleteContentParams) error
DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsParams) error
GetAllContent(ctx context.Context, siteID string) ([]Content, error)

View File

@@ -3,7 +3,8 @@
CREATE TABLE content (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
html_content TEXT NOT NULL,
original_template TEXT,
type TEXT NOT NULL,
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
@@ -16,7 +17,8 @@ CREATE TABLE content_versions (
version_id SERIAL PRIMARY KEY,
content_id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
html_content TEXT NOT NULL,
original_template TEXT,
type TEXT NOT NULL,
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL

View File

@@ -2,8 +2,9 @@
CREATE TABLE IF NOT EXISTS content (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')),
html_content TEXT NOT NULL,
original_template TEXT,
type TEXT NOT NULL CHECK (type IN ('text', 'link')),
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
updated_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
@@ -15,7 +16,8 @@ CREATE TABLE IF NOT EXISTS content_versions (
version_id SERIAL PRIMARY KEY,
content_id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
html_content TEXT NOT NULL,
original_template TEXT,
type TEXT NOT NULL,
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
// source: setup.sql
package postgresql
@@ -55,8 +55,9 @@ const initializeSchema = `-- name: InitializeSchema :exec
CREATE TABLE IF NOT EXISTS content (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')),
html_content TEXT NOT NULL,
original_template TEXT,
type TEXT NOT NULL CHECK (type IN ('text', 'link')),
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
updated_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
@@ -74,7 +75,8 @@ CREATE TABLE IF NOT EXISTS content_versions (
version_id SERIAL PRIMARY KEY,
content_id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
html_content TEXT NOT NULL,
original_template TEXT,
type TEXT NOT NULL,
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
// source: versions.sql
package postgresql
@@ -11,23 +11,25 @@ import (
)
const createContentVersion = `-- name: CreateContentVersion :exec
INSERT INTO content_versions (content_id, site_id, value, type, created_by)
VALUES ($1, $2, $3, $4, $5)
INSERT INTO content_versions (content_id, site_id, html_content, original_template, type, created_by)
VALUES ($1, $2, $3, $4, $5, $6)
`
type CreateContentVersionParams struct {
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedBy string `json:"created_by"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"`
CreatedBy string `json:"created_by"`
}
func (q *Queries) CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error {
_, err := q.db.ExecContext(ctx, createContentVersion,
arg.ContentID,
arg.SiteID,
arg.Value,
arg.HtmlContent,
arg.OriginalTemplate,
arg.Type,
arg.CreatedBy,
)
@@ -51,8 +53,8 @@ func (q *Queries) DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsPa
const getAllVersionsForSite = `-- name: GetAllVersionsForSite :many
SELECT
cv.version_id, cv.content_id, cv.site_id, cv.value, cv.type, cv.created_at, cv.created_by,
c.value as current_value
cv.version_id, cv.content_id, cv.site_id, cv.html_content, cv.original_template, cv.type, cv.created_at, cv.created_by,
c.html_content as current_html_content
FROM content_versions cv
LEFT JOIN content c ON cv.content_id = c.id AND cv.site_id = c.site_id
WHERE cv.site_id = $1
@@ -66,14 +68,15 @@ type GetAllVersionsForSiteParams struct {
}
type GetAllVersionsForSiteRow struct {
VersionID int32 `json:"version_id"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
CurrentValue sql.NullString `json:"current_value"`
VersionID int32 `json:"version_id"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
CurrentHtmlContent sql.NullString `json:"current_html_content"`
}
func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsForSiteParams) ([]GetAllVersionsForSiteRow, error) {
@@ -89,11 +92,12 @@ func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsF
&i.VersionID,
&i.ContentID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.CreatedBy,
&i.CurrentValue,
&i.CurrentHtmlContent,
); err != nil {
return nil, err
}
@@ -109,7 +113,7 @@ func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsF
}
const getContentVersion = `-- name: GetContentVersion :one
SELECT version_id, content_id, site_id, value, type, created_at, created_by
SELECT version_id, content_id, site_id, html_content, original_template, type, created_at, created_by
FROM content_versions
WHERE version_id = $1
`
@@ -121,7 +125,8 @@ func (q *Queries) GetContentVersion(ctx context.Context, versionID int32) (Conte
&i.VersionID,
&i.ContentID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.CreatedBy,
@@ -130,7 +135,7 @@ func (q *Queries) GetContentVersion(ctx context.Context, versionID int32) (Conte
}
const getContentVersionHistory = `-- name: GetContentVersionHistory :many
SELECT version_id, content_id, site_id, value, type, created_at, created_by
SELECT version_id, content_id, site_id, html_content, original_template, type, created_at, created_by
FROM content_versions
WHERE content_id = $1 AND site_id = $2
ORDER BY created_at DESC
@@ -156,7 +161,8 @@ func (q *Queries) GetContentVersionHistory(ctx context.Context, arg GetContentVe
&i.VersionID,
&i.ContentID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.CreatedBy,

View File

@@ -1,39 +1,43 @@
-- name: GetContent :one
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
FROM content
WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id);
-- name: GetAllContent :many
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
FROM content
WHERE site_id = sqlc.arg(site_id)
ORDER BY updated_at DESC;
-- name: GetBulkContent :many
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
FROM content
WHERE site_id = sqlc.arg(site_id) AND id IN (sqlc.slice('ids'));
-- name: CreateContent :one
INSERT INTO content (id, site_id, value, type, last_edited_by)
VALUES (sqlc.arg(id), sqlc.arg(site_id), sqlc.arg(value), sqlc.arg(type), sqlc.arg(last_edited_by))
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by;
INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by)
VALUES (sqlc.arg(id), sqlc.arg(site_id), sqlc.arg(html_content), sqlc.arg(original_template), sqlc.arg(type), sqlc.arg(last_edited_by))
RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by;
-- name: UpdateContent :one
UPDATE content
SET value = sqlc.arg(value), type = sqlc.arg(type), last_edited_by = sqlc.arg(last_edited_by)
SET html_content = sqlc.arg(html_content), type = sqlc.arg(type), last_edited_by = sqlc.arg(last_edited_by)
WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id)
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by;
RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by;
-- name: UpsertContent :one
INSERT INTO content (id, site_id, value, type, last_edited_by)
VALUES (sqlc.arg(id), sqlc.arg(site_id), sqlc.arg(value), sqlc.arg(type), sqlc.arg(last_edited_by))
INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by)
VALUES (sqlc.arg(id), sqlc.arg(site_id), sqlc.arg(html_content), sqlc.arg(original_template), sqlc.arg(type), sqlc.arg(last_edited_by))
ON CONFLICT(id, site_id) DO UPDATE SET
value = EXCLUDED.value,
html_content = EXCLUDED.html_content,
type = EXCLUDED.type,
last_edited_by = EXCLUDED.last_edited_by
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by;
RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by;
-- name: DeleteContent :exec
DELETE FROM content
WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id);
WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id);
-- name: DeleteAllSiteContent :exec
DELETE FROM content
WHERE site_id = sqlc.arg(site_id);

View File

@@ -1,23 +1,23 @@
-- name: CreateContentVersion :exec
INSERT INTO content_versions (content_id, site_id, value, type, created_by)
VALUES (sqlc.arg(content_id), sqlc.arg(site_id), sqlc.arg(value), sqlc.arg(type), sqlc.arg(created_by));
INSERT INTO content_versions (content_id, site_id, html_content, original_template, type, created_by)
VALUES (sqlc.arg(content_id), sqlc.arg(site_id), sqlc.arg(html_content), sqlc.arg(original_template), sqlc.arg(type), sqlc.arg(created_by));
-- name: GetContentVersionHistory :many
SELECT version_id, content_id, site_id, value, type, created_at, created_by
SELECT version_id, content_id, site_id, html_content, original_template, type, created_at, created_by
FROM content_versions
WHERE content_id = sqlc.arg(content_id) AND site_id = sqlc.arg(site_id)
ORDER BY created_at DESC
LIMIT sqlc.arg(limit_count);
-- name: GetContentVersion :one
SELECT version_id, content_id, site_id, value, type, created_at, created_by
SELECT version_id, content_id, site_id, html_content, original_template, type, created_at, created_by
FROM content_versions
WHERE version_id = sqlc.arg(version_id);
-- name: GetAllVersionsForSite :many
SELECT
cv.version_id, cv.content_id, cv.site_id, cv.value, cv.type, cv.created_at, cv.created_by,
c.value as current_value
cv.version_id, cv.content_id, cv.site_id, cv.html_content, cv.original_template, cv.type, cv.created_at, cv.created_by,
c.html_content as current_html_content
FROM content_versions cv
LEFT JOIN content c ON cv.content_id = c.id AND cv.site_id = c.site_id
WHERE cv.site_id = sqlc.arg(site_id)

View File

@@ -1,34 +1,37 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
// source: content.sql
package sqlite
import (
"context"
"database/sql"
"strings"
)
const createContent = `-- name: CreateContent :one
INSERT INTO content (id, site_id, value, type, last_edited_by)
VALUES (?1, ?2, ?3, ?4, ?5)
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by
INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
`
type CreateContentParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"`
}
func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (Content, error) {
row := q.db.QueryRowContext(ctx, createContent,
arg.ID,
arg.SiteID,
arg.Value,
arg.HtmlContent,
arg.OriginalTemplate,
arg.Type,
arg.LastEditedBy,
)
@@ -36,7 +39,8 @@ func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (C
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
@@ -45,6 +49,16 @@ func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (C
return i, err
}
const deleteAllSiteContent = `-- name: DeleteAllSiteContent :exec
DELETE FROM content
WHERE site_id = ?1
`
func (q *Queries) DeleteAllSiteContent(ctx context.Context, siteID string) error {
_, err := q.db.ExecContext(ctx, deleteAllSiteContent, siteID)
return err
}
const deleteContent = `-- name: DeleteContent :exec
DELETE FROM content
WHERE id = ?1 AND site_id = ?2
@@ -61,7 +75,7 @@ func (q *Queries) DeleteContent(ctx context.Context, arg DeleteContentParams) er
}
const getAllContent = `-- name: GetAllContent :many
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
FROM content
WHERE site_id = ?1
ORDER BY updated_at DESC
@@ -79,7 +93,8 @@ func (q *Queries) GetAllContent(ctx context.Context, siteID string) ([]Content,
if err := rows.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
@@ -99,7 +114,7 @@ func (q *Queries) GetAllContent(ctx context.Context, siteID string) ([]Content,
}
const getBulkContent = `-- name: GetBulkContent :many
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
FROM content
WHERE site_id = ?1 AND id IN (/*SLICE:ids*/?)
`
@@ -132,7 +147,8 @@ func (q *Queries) GetBulkContent(ctx context.Context, arg GetBulkContentParams)
if err := rows.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
@@ -152,7 +168,7 @@ func (q *Queries) GetBulkContent(ctx context.Context, arg GetBulkContentParams)
}
const getContent = `-- name: GetContent :one
SELECT id, site_id, value, type, created_at, updated_at, last_edited_by
SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
FROM content
WHERE id = ?1 AND site_id = ?2
`
@@ -168,7 +184,8 @@ func (q *Queries) GetContent(ctx context.Context, arg GetContentParams) (Content
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
@@ -179,13 +196,13 @@ func (q *Queries) GetContent(ctx context.Context, arg GetContentParams) (Content
const updateContent = `-- name: UpdateContent :one
UPDATE content
SET value = ?1, type = ?2, last_edited_by = ?3
SET html_content = ?1, type = ?2, last_edited_by = ?3
WHERE id = ?4 AND site_id = ?5
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by
RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
`
type UpdateContentParams struct {
Value string `json:"value"`
HtmlContent string `json:"html_content"`
Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"`
@@ -194,7 +211,7 @@ type UpdateContentParams struct {
func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error) {
row := q.db.QueryRowContext(ctx, updateContent,
arg.Value,
arg.HtmlContent,
arg.Type,
arg.LastEditedBy,
arg.ID,
@@ -204,7 +221,8 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,
@@ -214,28 +232,30 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C
}
const upsertContent = `-- name: UpsertContent :one
INSERT INTO content (id, site_id, value, type, last_edited_by)
VALUES (?1, ?2, ?3, ?4, ?5)
INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT(id, site_id) DO UPDATE SET
value = EXCLUDED.value,
html_content = EXCLUDED.html_content,
type = EXCLUDED.type,
last_edited_by = EXCLUDED.last_edited_by
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by
RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by
`
type UpsertContentParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"`
}
func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) {
row := q.db.QueryRowContext(ctx, upsertContent,
arg.ID,
arg.SiteID,
arg.Value,
arg.HtmlContent,
arg.OriginalTemplate,
arg.Type,
arg.LastEditedBy,
)
@@ -243,7 +263,8 @@ func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (C
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.UpdatedAt,

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
package sqlite

View File

@@ -1,25 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
package sqlite
import (
"database/sql"
)
type Content struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
}
type ContentVersion struct {
VersionID int64 `json:"version_id"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
VersionID int64 `json:"version_id"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
}

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
package sqlite
@@ -11,6 +11,7 @@ import (
type Querier interface {
CreateContent(ctx context.Context, arg CreateContentParams) (Content, error)
CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error
DeleteAllSiteContent(ctx context.Context, siteID string) error
DeleteContent(ctx context.Context, arg DeleteContentParams) error
DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsParams) error
GetAllContent(ctx context.Context, siteID string) ([]Content, error)

View File

@@ -3,7 +3,8 @@
CREATE TABLE content (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
html_content TEXT NOT NULL,
original_template TEXT,
type TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
@@ -16,7 +17,8 @@ CREATE TABLE content_versions (
version_id INTEGER PRIMARY KEY AUTOINCREMENT,
content_id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
html_content TEXT NOT NULL,
original_template TEXT,
type TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL

View File

@@ -2,8 +2,9 @@
CREATE TABLE IF NOT EXISTS content (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')),
html_content TEXT NOT NULL,
original_template TEXT,
type TEXT NOT NULL CHECK (type IN ('text', 'link')),
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
@@ -15,7 +16,8 @@ CREATE TABLE IF NOT EXISTS content_versions (
version_id INTEGER PRIMARY KEY AUTOINCREMENT,
content_id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
html_content TEXT NOT NULL,
original_template TEXT,
type TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
// source: setup.sql
package sqlite
@@ -13,8 +13,9 @@ const initializeSchema = `-- name: InitializeSchema :exec
CREATE TABLE IF NOT EXISTS content (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')),
html_content TEXT NOT NULL,
original_template TEXT,
type TEXT NOT NULL CHECK (type IN ('text', 'link')),
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
@@ -32,7 +33,8 @@ CREATE TABLE IF NOT EXISTS content_versions (
version_id INTEGER PRIMARY KEY AUTOINCREMENT,
content_id TEXT NOT NULL,
site_id TEXT NOT NULL,
value TEXT NOT NULL,
html_content TEXT NOT NULL,
original_template TEXT,
type TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL

View File

@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
// source: versions.sql
package sqlite
@@ -11,23 +11,25 @@ import (
)
const createContentVersion = `-- name: CreateContentVersion :exec
INSERT INTO content_versions (content_id, site_id, value, type, created_by)
VALUES (?1, ?2, ?3, ?4, ?5)
INSERT INTO content_versions (content_id, site_id, html_content, original_template, type, created_by)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
`
type CreateContentVersionParams struct {
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedBy string `json:"created_by"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"`
CreatedBy string `json:"created_by"`
}
func (q *Queries) CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error {
_, err := q.db.ExecContext(ctx, createContentVersion,
arg.ContentID,
arg.SiteID,
arg.Value,
arg.HtmlContent,
arg.OriginalTemplate,
arg.Type,
arg.CreatedBy,
)
@@ -51,8 +53,8 @@ func (q *Queries) DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsPa
const getAllVersionsForSite = `-- name: GetAllVersionsForSite :many
SELECT
cv.version_id, cv.content_id, cv.site_id, cv.value, cv.type, cv.created_at, cv.created_by,
c.value as current_value
cv.version_id, cv.content_id, cv.site_id, cv.html_content, cv.original_template, cv.type, cv.created_at, cv.created_by,
c.html_content as current_html_content
FROM content_versions cv
LEFT JOIN content c ON cv.content_id = c.id AND cv.site_id = c.site_id
WHERE cv.site_id = ?1
@@ -66,14 +68,15 @@ type GetAllVersionsForSiteParams struct {
}
type GetAllVersionsForSiteRow struct {
VersionID int64 `json:"version_id"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
CurrentValue sql.NullString `json:"current_value"`
VersionID int64 `json:"version_id"`
ContentID string `json:"content_id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
CurrentHtmlContent sql.NullString `json:"current_html_content"`
}
func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsForSiteParams) ([]GetAllVersionsForSiteRow, error) {
@@ -89,11 +92,12 @@ func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsF
&i.VersionID,
&i.ContentID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.CreatedBy,
&i.CurrentValue,
&i.CurrentHtmlContent,
); err != nil {
return nil, err
}
@@ -109,7 +113,7 @@ func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsF
}
const getContentVersion = `-- name: GetContentVersion :one
SELECT version_id, content_id, site_id, value, type, created_at, created_by
SELECT version_id, content_id, site_id, html_content, original_template, type, created_at, created_by
FROM content_versions
WHERE version_id = ?1
`
@@ -121,7 +125,8 @@ func (q *Queries) GetContentVersion(ctx context.Context, versionID int64) (Conte
&i.VersionID,
&i.ContentID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.CreatedBy,
@@ -130,7 +135,7 @@ func (q *Queries) GetContentVersion(ctx context.Context, versionID int64) (Conte
}
const getContentVersionHistory = `-- name: GetContentVersionHistory :many
SELECT version_id, content_id, site_id, value, type, created_at, created_by
SELECT version_id, content_id, site_id, html_content, original_template, type, created_at, created_by
FROM content_versions
WHERE content_id = ?1 AND site_id = ?2
ORDER BY created_at DESC
@@ -156,7 +161,8 @@ func (q *Queries) GetContentVersionHistory(ctx context.Context, arg GetContentVe
&i.VersionID,
&i.ContentID,
&i.SiteID,
&i.Value,
&i.HtmlContent,
&i.OriginalTemplate,
&i.Type,
&i.CreatedAt,
&i.CreatedBy,

View File

@@ -2,6 +2,7 @@ package engine
import (
"context"
"database/sql"
"fmt"
"github.com/insertr/insertr/internal/db"
@@ -9,6 +10,14 @@ import (
"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
@@ -33,11 +42,12 @@ func (c *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err
return nil, err
}
return &ContentItem{
ID: content.ID,
SiteID: content.SiteID,
Value: content.Value,
Type: content.Type,
LastEditedBy: content.LastEditedBy,
ID: content.ID,
SiteID: content.SiteID,
HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type,
LastEditedBy: content.LastEditedBy,
}, nil
case "postgresql":
@@ -49,11 +59,12 @@ func (c *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err
return nil, err
}
return &ContentItem{
ID: content.ID,
SiteID: content.SiteID,
Value: content.Value,
Type: content.Type,
LastEditedBy: content.LastEditedBy,
ID: content.ID,
SiteID: content.SiteID,
HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type,
LastEditedBy: content.LastEditedBy,
}, nil
default:
@@ -76,11 +87,12 @@ func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
items := make(map[string]ContentItem)
for _, content := range contents {
items[content.ID] = ContentItem{
ID: content.ID,
SiteID: content.SiteID,
Value: content.Value,
Type: content.Type,
LastEditedBy: content.LastEditedBy,
ID: content.ID,
SiteID: content.SiteID,
HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type,
LastEditedBy: content.LastEditedBy,
}
}
return items, nil
@@ -97,11 +109,12 @@ func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
items := make(map[string]ContentItem)
for _, content := range contents {
items[content.ID] = ContentItem{
ID: content.ID,
SiteID: content.SiteID,
Value: content.Value,
Type: content.Type,
LastEditedBy: content.LastEditedBy,
ID: content.ID,
SiteID: content.SiteID,
HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type,
LastEditedBy: content.LastEditedBy,
}
}
return items, nil
@@ -123,11 +136,12 @@ func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
items := make(map[string]ContentItem)
for _, content := range contents {
items[content.ID] = ContentItem{
ID: content.ID,
SiteID: content.SiteID,
Value: content.Value,
Type: content.Type,
LastEditedBy: content.LastEditedBy,
ID: content.ID,
SiteID: content.SiteID,
HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type,
LastEditedBy: content.LastEditedBy,
}
}
return items, nil
@@ -141,11 +155,12 @@ func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
items := make(map[string]ContentItem)
for _, content := range contents {
items[content.ID] = ContentItem{
ID: content.ID,
SiteID: content.SiteID,
Value: content.Value,
Type: content.Type,
LastEditedBy: content.LastEditedBy,
ID: content.ID,
SiteID: content.SiteID,
HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type,
LastEditedBy: content.LastEditedBy,
}
}
return items, nil
@@ -154,3 +169,61 @@ func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
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, contentType, 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),
Type: contentType,
LastEditedBy: lastEditedBy,
})
if err != nil {
return nil, err
}
return &ContentItem{
ID: content.ID,
SiteID: content.SiteID,
HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type,
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),
Type: contentType,
LastEditedBy: lastEditedBy,
})
if err != nil {
return nil, err
}
return &ContentItem{
ID: content.ID,
SiteID: content.SiteID,
HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type,
LastEditedBy: content.LastEditedBy,
}, nil
default:
return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType())
}
}
// Helper function to convert string to sql.NullString
func toNullString(s string) sql.NullString {
if s == "" {
return sql.NullString{Valid: false}
}
return sql.NullString{String: s, Valid: true}
}

View File

@@ -17,14 +17,17 @@ type ContentEngine struct {
idGenerator *IDGenerator
client ContentClient
authProvider *AuthProvider
injector *Injector
}
// NewContentEngine creates a new content processing engine
func NewContentEngine(client ContentClient) *ContentEngine {
authProvider := &AuthProvider{Type: "mock"} // default
return &ContentEngine{
idGenerator: NewIDGenerator(),
client: client,
authProvider: &AuthProvider{Type: "mock"}, // default
authProvider: authProvider,
injector: NewInjector(client, ""), // siteID will be set per operation
}
}
@@ -37,6 +40,7 @@ func NewContentEngineWithAuth(client ContentClient, authProvider *AuthProvider)
idGenerator: NewIDGenerator(),
client: client,
authProvider: authProvider,
injector: NewInjectorWithAuth(client, "", authProvider), // siteID will be set per operation
}
}
@@ -84,6 +88,20 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
// Add/update content attributes to the node
e.addContentAttributes(elem.Node, id, elem.Type)
// Store content and template for newly discovered elements (first-pass)
if wasGenerated && (input.Mode == Enhancement || input.Mode == ContentInjection) {
// Extract content and template from the unprocessed element
htmlContent := e.extractHTMLContent(elem.Node)
originalTemplate := e.extractOriginalTemplate(elem.Node)
// Store in database via content client
_, err := e.client.CreateContent(input.SiteID, id, htmlContent, originalTemplate, elem.Type, "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)
}
}
}
// 4. Inject content if required by mode
@@ -157,7 +175,7 @@ func (e *ContentEngine) determineContentType(node *html.Node) string {
case "h1", "h2", "h3", "h4", "h5", "h6":
return "text"
case "p", "div", "section", "article", "span":
return "markdown"
return "text"
default:
return "text"
}
@@ -211,28 +229,35 @@ func (e *ContentEngine) injectContent(elements []ProcessedElement, siteID string
if contentItem != nil {
// Inject the content into the element
elem.Content = contentItem.Value
e.injectContentIntoNode(elem.Node, contentItem.Value, contentItem.Type)
elem.Content = contentItem.HTMLContent
// Update injector siteID for this operation
e.injector.siteID = siteID
e.injector.injectHTMLContent(elem.Node, contentItem.HTMLContent)
}
}
return nil
}
// injectContentIntoNode injects content value into an HTML node
func (e *ContentEngine) injectContentIntoNode(node *html.Node, content, contentType string) {
// Clear existing text content
for child := node.FirstChild; child != nil; {
next := child.NextSibling
if child.Type == html.TextNode {
node.RemoveChild(child)
// extractHTMLContent extracts the inner HTML content from a node
func (e *ContentEngine) extractHTMLContent(node *html.Node) string {
var content strings.Builder
// Render all child nodes in order to preserve HTML structure
for child := node.FirstChild; child != nil; child = child.NextSibling {
if err := html.Render(&content, child); err == nil {
// All nodes (text and element) rendered in correct order
}
child = next
}
// Add new text content
textNode := &html.Node{
Type: html.TextNode,
Data: content,
}
node.AppendChild(textNode)
return strings.TrimSpace(content.String())
}
// extractOriginalTemplate extracts the outer HTML of the element (including the element itself)
func (e *ContentEngine) extractOriginalTemplate(node *html.Node) string {
var buf strings.Builder
if err := html.Render(&buf, node); err != nil {
return ""
}
return buf.String()
}

View File

@@ -1,12 +1,11 @@
package engine
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"path/filepath"
"strings"
"github.com/google/uuid"
"golang.org/x/net/html"
)
@@ -36,12 +35,10 @@ func (g *IDGenerator) Generate(node *html.Node, filePath string) string {
// 3. Build readable prefix (deterministic, no runtime counting)
prefix := g.buildDeterministicPrefix(fileName, tag, primaryClass)
// 5. Add collision-resistant suffix
signature := g.createSignature(node, filePath)
hash := sha256.Sum256([]byte(signature))
suffix := hex.EncodeToString(hash[:3])
// 5. Add UUID-based suffix for guaranteed uniqueness
uuidSuffix := uuid.New().String()[:6] // Use first 6 chars of UUID
finalID := fmt.Sprintf("%s-%s", prefix, suffix)
finalID := fmt.Sprintf("%s-%s", prefix, uuidSuffix)
// Ensure uniqueness (should be guaranteed by hash, but safety check)
g.usedIDs[finalID] = true
@@ -114,14 +111,10 @@ func (g *IDGenerator) buildPrefix(fileName, tag, primaryClass string, index int)
return strings.Join(parts, "-")
}
// createSignature creates a unique signature for collision resistance
// createSignature creates a unique signature for collision resistance (DEPRECATED - using UUID now)
func (g *IDGenerator) createSignature(node *html.Node, filePath string) string {
// Minimal signature for uniqueness
tag := node.Data
classes := strings.Join(GetClasses(node), " ")
domPath := g.getSimpleDOMPath(node)
return fmt.Sprintf("%s|%s|%s|%s", filePath, domPath, tag, classes)
// This method is kept for compatibility but not used in UUID-based generation
return ""
}
// getSimpleDOMPath creates a simple DOM path for uniqueness
@@ -142,3 +135,68 @@ func (g *IDGenerator) getSimpleDOMPath(node *html.Node) string {
return strings.Join(pathParts, ">")
}
// getContentPreview extracts first 50 characters of text content for uniqueness
func (g *IDGenerator) getContentPreview(node *html.Node) string {
var text strings.Builder
g.extractTextContent(node, &text)
content := strings.TrimSpace(text.String())
if len(content) > 50 {
content = content[:50]
}
// Remove newlines and normalize whitespace
content = strings.ReplaceAll(content, "\n", " ")
content = strings.ReplaceAll(content, "\t", " ")
for strings.Contains(content, " ") {
content = strings.ReplaceAll(content, " ", " ")
}
return content
}
// extractTextContent recursively extracts text content from a node
func (g *IDGenerator) extractTextContent(node *html.Node, text *strings.Builder) {
if node.Type == html.TextNode {
text.WriteString(node.Data)
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
g.extractTextContent(child, text)
}
}
// getSiblingIndex returns the position of this element among its siblings of the same type
func (g *IDGenerator) getSiblingIndex(node *html.Node) int {
if node.Parent == nil {
return 0
}
index := 0
tag := node.Data
classes := GetClasses(node)
for sibling := node.Parent.FirstChild; sibling != nil; sibling = sibling.NextSibling {
if sibling.Type == html.ElementNode && sibling.Data == tag {
siblingClasses := GetClasses(sibling)
// Check if classes match (for more precise positioning)
if g.classesMatch(classes, siblingClasses) {
if sibling == node {
return index
}
index++
}
}
}
return index
}
// classesMatch checks if two class lists are equivalent
func (g *IDGenerator) classesMatch(classes1, classes2 []string) bool {
if len(classes1) != len(classes2) {
return false
}
for i, class := range classes1 {
if i >= len(classes2) || class != classes2[i] {
return false
}
}
return true
}

View File

@@ -12,7 +12,6 @@ import (
type Injector struct {
client ContentClient
siteID string
mdProcessor *MarkdownProcessor
authProvider *AuthProvider
}
@@ -21,7 +20,6 @@ func NewInjector(client ContentClient, siteID string) *Injector {
return &Injector{
client: client,
siteID: siteID,
mdProcessor: NewMarkdownProcessor(),
authProvider: &AuthProvider{Type: "mock"}, // default
}
}
@@ -34,7 +32,6 @@ func NewInjectorWithAuth(client ContentClient, siteID string, authProvider *Auth
return &Injector{
client: client,
siteID: siteID,
mdProcessor: NewMarkdownProcessor(),
authProvider: authProvider,
}
}
@@ -53,17 +50,8 @@ func (i *Injector) InjectContent(element *Element, contentID string) error {
return nil
}
// Replace element content based on type
switch element.Type {
case "text":
i.injectTextContent(element.Node, contentItem.Value)
case "markdown":
i.injectMarkdownContent(element.Node, contentItem.Value)
case "link":
i.injectLinkContent(element.Node, contentItem.Value)
default:
i.injectTextContent(element.Node, contentItem.Value)
}
// Direct HTML injection for all content types
i.injectHTMLContent(element.Node, contentItem.HTMLContent)
// Add data attributes for editor functionality
i.AddContentAttributes(element.Node, contentID, element.Type)
@@ -97,65 +85,13 @@ func (i *Injector) InjectBulkContent(elements []ElementWithID) error {
continue
}
// Replace content based on type
switch elem.Element.Type {
case "text":
i.injectTextContent(elem.Element.Node, contentItem.Value)
case "markdown":
i.injectMarkdownContent(elem.Element.Node, contentItem.Value)
case "link":
i.injectLinkContent(elem.Element.Node, contentItem.Value)
default:
i.injectTextContent(elem.Element.Node, contentItem.Value)
}
// Direct HTML injection for all content types
i.injectHTMLContent(elem.Element.Node, contentItem.HTMLContent)
}
return nil
}
// injectTextContent replaces text content in an element
func (i *Injector) injectTextContent(node *html.Node, content string) {
// Remove all child nodes
for child := node.FirstChild; child != nil; {
next := child.NextSibling
node.RemoveChild(child)
child = next
}
// Add new text content
textNode := &html.Node{
Type: html.TextNode,
Data: content,
}
node.AppendChild(textNode)
}
// injectMarkdownContent handles markdown content - converts markdown to HTML
func (i *Injector) injectMarkdownContent(node *html.Node, content string) {
if content == "" {
i.injectTextContent(node, "")
return
}
// Convert markdown to HTML using server processor
htmlContent, err := i.mdProcessor.ToHTML(content)
if err != nil {
log.Printf("⚠️ Markdown conversion failed for content '%s': %v, falling back to text", content, err)
i.injectTextContent(node, content)
return
}
// Inject the HTML content
i.injectHTMLContent(node, htmlContent)
}
// injectLinkContent handles link/button content with URL extraction
func (i *Injector) injectLinkContent(node *html.Node, content string) {
// For now, just inject the text content
// TODO: Parse content for URL and text components
i.injectTextContent(node, content)
}
// injectHTMLContent safely injects HTML content into a DOM node
// Preserves the original element and only replaces its content
func (i *Injector) injectHTMLContent(node *html.Node, htmlContent string) {
@@ -172,8 +108,14 @@ func (i *Injector) injectHTMLContent(node *html.Node, htmlContent string) {
// Parse HTML string
doc, err := html.Parse(strings.NewReader(wrappedHTML))
if err != nil {
log.Printf("Failed to parse HTML content '%s': %v, falling back to text", htmlContent, err)
i.injectTextContent(node, htmlContent)
log.Printf("Failed to parse HTML content '%s': %v, falling back to text node", htmlContent, err)
// Fallback: inject as text node
i.clearNode(node)
textNode := &html.Node{
Type: html.TextNode,
Data: htmlContent,
}
node.AppendChild(textNode)
return
}

View File

@@ -1,76 +0,0 @@
package engine
import (
"bytes"
"log"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
// MarkdownProcessor handles minimal markdown processing
// Supports only: **bold**, *italic*, and [link](url)
type MarkdownProcessor struct {
parser goldmark.Markdown
}
// NewMarkdownProcessor creates a new markdown processor with minimal configuration
func NewMarkdownProcessor() *MarkdownProcessor {
// Configure goldmark to only support basic inline formatting
md := goldmark.New(
goldmark.WithParserOptions(
parser.WithInlineParsers(
// Bold (**text**) and italic (*text*) - same parser handles both
util.Prioritized(parser.NewEmphasisParser(), 500),
// Links [text](url)
util.Prioritized(parser.NewLinkParser(), 600),
),
// Disable all block parsers except paragraph (no headings, lists, etc.)
parser.WithBlockParsers(
util.Prioritized(parser.NewParagraphParser(), 200),
),
),
goldmark.WithRendererOptions(
html.WithXHTML(), // <br /> instead of <br>
html.WithHardWraps(), // Line breaks become <br />
html.WithUnsafe(), // Allow existing HTML to pass through
),
)
return &MarkdownProcessor{parser: md}
}
// ToHTML converts markdown string to HTML
func (mp *MarkdownProcessor) ToHTML(markdown string) (string, error) {
if markdown == "" {
return "", nil
}
var buf bytes.Buffer
if err := mp.parser.Convert([]byte(markdown), &buf); err != nil {
log.Printf("Markdown conversion failed: %v", err)
return "", err
}
html := buf.String()
// Clean up goldmark's paragraph wrapping for inline content
// If content is wrapped in a single <p> tag, extract just the inner content
html = strings.TrimSpace(html)
if strings.HasPrefix(html, "<p>") && strings.HasSuffix(html, "</p>") {
// Check if this is a single paragraph (no other <p> tags inside)
inner := html[3 : len(html)-4] // Remove <p> and </p>
if !strings.Contains(inner, "<p>") {
// Single paragraph - return just the inner content for inline injection
return inner, nil
}
}
// Multiple paragraphs or other block content - return as-is
return html, nil
}

View File

@@ -35,7 +35,7 @@ type ContentResult struct {
type ProcessedElement struct {
Node *html.Node // HTML node
ID string // Generated content ID
Type string // Content type (text, markdown, link)
Type string // Content type (text, link)
Content string // Injected content (if any)
Generated bool // Whether ID was generated (vs existing)
Tag string // Element tag name
@@ -48,16 +48,18 @@ 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, contentType, lastEditedBy string) (*ContentItem, error)
}
// ContentItem represents a piece of content from the database
type ContentItem struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
Value string `json:"value"`
Type string `json:"type"`
UpdatedAt string `json:"updated_at"`
LastEditedBy string `json:"last_edited_by,omitempty"`
ID string `json:"id"`
SiteID string `json:"site_id"`
HTMLContent string `json:"html_content"`
OriginalTemplate string `json:"original_template"`
Type string `json:"type"`
UpdatedAt string `json:"updated_at"`
LastEditedBy string `json:"last_edited_by,omitempty"`
}
// ContentResponse represents the API response structure