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

@@ -4,229 +4,263 @@
### **What We Discovered** ### **What We Discovered**
Our frontend has evolved to a sophisticated **HTML-first approach** with: Our frontend has evolved to a sophisticated **HTML-first approach** with:
- Style-aware editing with automatic style detection - StyleAware editor with automatic style detection from nested elements
- HTML preservation with perfect attribute fidelity - HTML preservation with perfect attribute fidelity
- Rich content editing capabilities - Rich content editing capabilities with formatting toolbar
- Template-based style preservation - Template-based style preservation using CLASSES.md methodology
However, our **server API is still text-focused**, creating a fundamental mismatch between frontend capabilities and backend storage. However, our **server API is still text-focused**, creating a fundamental mismatch between frontend capabilities and backend storage.
### **Core Issues Identified** ### **Core Requirements Identified**
1. **Storage Mismatch**: Server stores plain text (`value`), frontend produces rich HTML 1. **HTML-First Storage**: Replace `value` with `html_content` field for direct HTML storage
2. **Style Loss**: Developer-defined styles disappear when unused by editors 2. **Template Preservation**: Store `original_template` for consistent style detection
3. **Template Preservation**: Need to maintain original developer markup for style detection 3. **Enhancer-First Workflow**: Enhancer stores content on first pass, ignores processed elements
4. **Dual Mode Challenge**: Development iteration vs. production stability requirements 4. **No Markdown Processing**: Remove all markdown logic from injector - HTML only
5. **StyleAware Editor Compatibility**: API must match library expectations
6. **Dev Convenience**: Option to clean DB for fresh development iterations
## 🏗️ Proposed Architecture Changes ## 🏗️ Implementation Strategy
### **1. HTML-First Database Schema** ### **1. HTML-First Database Schema (Direct Replacement)**
**Updated Schema (No Backwards Compatibility Required):** **Updated Schema:**
```sql ```sql
-- SQLite schema
CREATE TABLE content ( CREATE TABLE content (
id TEXT PRIMARY KEY, id TEXT NOT NULL,
site_id TEXT NOT NULL, site_id TEXT NOT NULL,
html_content TEXT NOT NULL, -- Rich HTML (for BOTH editing AND injection) html_content TEXT NOT NULL, -- Rich HTML content (innerHTML)
original_markup TEXT, -- Developer template markup (for style detection) original_template TEXT, -- Original element markup for style detection (outerHTML)
template_locked BOOLEAN DEFAULT FALSE, -- Development vs Production mode
type TEXT NOT NULL, type TEXT NOT NULL,
created_at INTEGER NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER NOT NULL, updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
last_edited_by TEXT NOT NULL, last_edited_by TEXT DEFAULT 'system' NOT NULL,
UNIQUE(site_id, id) PRIMARY KEY (id, site_id)
); );
CREATE TABLE content_versions ( -- PostgreSQL schema
version_id INTEGER PRIMARY KEY AUTOINCREMENT, CREATE TABLE content (
content_id TEXT NOT NULL, id TEXT NOT NULL,
site_id TEXT NOT NULL, site_id TEXT NOT NULL,
html_content TEXT NOT NULL, -- Version HTML content html_content TEXT NOT NULL, -- Rich HTML content (innerHTML)
original_markup TEXT, -- Template at time of version original_template TEXT, -- Original element markup for style detection (outerHTML)
type TEXT NOT NULL, type TEXT NOT NULL,
created_at INTEGER NOT NULL, created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
created_by TEXT NOT NULL updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
); );
``` ```
**Key Changes:** **Key Changes:**
-**Removed `value` field** - HTML serves both editing and injection needs -**Removed `value` field** - HTML serves both editing and injection needs
-**Added `original_markup`** - Preserves developer templates for style detection -**Added `html_content`** - Direct HTML storage for content editing and injection
-**Added `template_locked`** - Controls template update behavior -**Added `original_template`** - Preserves developer templates for StyleAware editor style detection
-**Unified storage** - Same HTML content used for build injection and editing -**Simplified approach** - No complex template locking, focus on core functionality
### **2. Template Lifecycle Management** ### **2. Enhancer-First Workflow (First-Pass Processing)**
#### **Development Mode (template_locked = false):** #### **Unprocessed Element Detection:**
- Enhancement **updates templates** when developer markup changes significantly - Elements without `data-content-id` attribute are unprocessed
- API editing **preserves templates**, only updates content - Enhancer processes these elements and assigns IDs
- Supports rapid iteration and template refinement - Subsequent enhancer runs skip elements that already have `data-content-id`
#### **Production Mode (template_locked = true):** #### **Content Storage on First Pass:**
- Enhancement **preserves existing templates** regardless of markup changes
- API editing **never affects templates**
- Ensures developer styles always available to clients
#### **Template Management Commands:**
```bash
# Lock templates for production handoff
insertr templates lock --site-id mysite
# Edit specific template (opens in $EDITOR)
insertr templates edit --site-id mysite --content-id hero-title-abc123
# Show template status
insertr templates status --site-id mysite
```
### **3. Updated Content Processing Flow**
#### **Enhancement Process:**
```go ```go
func (e *ContentEngine) processElement(node *html.Node, siteID, contentID string, devMode bool) { func processElement(node *html.Node, siteID string) {
existingContent := getContent(siteID, contentID) // Check if already processed
currentMarkup := extractElementHTML(node) existingID := getAttribute(node, "data-content-id")
if existingID != "" {
return // Skip - already processed
}
if existingContent == nil { // Extract content and template
// First time: create with template contentID := generateContentID(node, filePath)
htmlContent := extractContentHTML(node) htmlContent := extractInnerHTML(node) // For editing/injection
createContent(siteID, contentID, htmlContent, currentMarkup, !devMode) originalTemplate := extractOuterHTML(node) // For style detection
} else if devMode && !existingContent.TemplateLocked { contentType := determineContentType(node)
// Dev mode: update template if changed, preserve content
if hasSignificantStyleChanges(existingContent.OriginalMarkup, currentMarkup) { // Store in database
updateTemplate(siteID, contentID, currentMarkup) createContent(siteID, contentID, htmlContent, originalTemplate, contentType)
}
} // Mark as processed
// Always inject existing html_content setAttribute(node, "data-content-id", contentID)
injectHTMLContent(node, existingContent.HTMLContent) setAttribute(node, "data-content-type", contentType)
} }
``` ```
#### **API Content Updates:** #### **Development Convenience:**
- Optional DB cleanup flag: `insertr enhance --clean-db` for fresh development iterations
- Allows developers to start with clean slate when refining site structure
### **3. Injector Redesign (HTML-Only)**
#### **Remove Markdown Processing:**
- Delete `MarkdownProcessor` and all markdown-related logic
- Direct HTML injection using existing `injectHTMLContent()` method
- Simplified injection flow focused on HTML fidelity
#### **Updated Injection Process:**
```go ```go
func (h *ContentHandler) CreateContent(req CreateContentRequest) { func (i *Injector) InjectContent(element *Element, contentID string) error {
// API updates only affect html_content, never original_markup // Fetch content from database
updateContent(req.SiteID, req.ID, req.HTMLContent) contentItem, err := i.client.GetContent(i.siteID, contentID)
// Template preservation handled automatically if err != nil || contentItem == nil {
// No content found - add attributes but keep original content
i.AddContentAttributes(element.Node, contentID, element.Type)
return nil
}
// Direct HTML injection - no markdown processing
i.injectHTMLContent(element.Node, contentItem.HTMLContent)
i.AddContentAttributes(element.Node, contentID, element.Type)
return nil
} }
``` ```
### **4. Frontend Integration** #### **Content Type Handling:**
- All content types use HTML storage and injection
- Remove type-specific processing (text, markdown, link)
- StyleAware editor handles rich editing based on element context
#### **Style Detection Using Templates:** ### **4. StyleAware Editor Integration**
```javascript
// Always use stored template for style detection
async initializeEditor(element) {
const response = await this.apiClient.getContent(element.dataset.contentId);
// Use original_markup for consistent style detection #### **API Response Format (Matches Editor Expectations):**
const templateHTML = response.original_markup || element.outerHTML;
this.styleEngine.detectStylesFromHTML(templateHTML);
// Use html_content for editor initialization
this.editor.setContent(response.html_content);
}
```
#### **Updated API Response:**
```json ```json
{ {
"id": "hero-title-abc123", "id": "hero-title-abc123",
"site_id": "mysite",
"html_content": "<h1>Welcome to <em>Our</em> Company</h1>", "html_content": "<h1>Welcome to <em>Our</em> Company</h1>",
"original_markup": "<h1 class=\"insertr\">Welcome to <span class=\"brand\">Our Company</span></h1>", "original_template": "<h1 class=\"insertr brand-heading\">Welcome to <span class=\"brand\">Our Company</span></h1>",
"template_locked": true, "type": "text",
"type": "text" "created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"last_edited_by": "user@example.com"
}
```
#### **Editor Integration:**
- **Style Detection**: Uses `original_template` for consistent formatting options
- **Content Editing**: Uses `html_content` for rich text editing
- **Perfect Alignment**: Response format matches StyleAware editor analysis requirements
- **Multi-Property Support**: Complex elements (links) work seamlessly with preserved templates
#### **Updated API Models:**
```go
type ContentItem struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
HTMLContent string `json:"html_content"` // For editor content
OriginalTemplate string `json:"original_template"` // For style detection
Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
} }
``` ```
## 🔄 Development Workflows ## 🔄 Development Workflows
### **Development Phase:** ### **Enhanced Development Workflow:**
```bash ```bash
# Start development (templates auto-update) # Start fresh development iteration
insertr enhance ./mysite --clean-db --site-id mysite
# Files are processed, content stored, elements marked as processed
# Subsequent enhancement runs skip already processed elements
# Developer can iterate on unprocessed parts without affecting existing content
# Start development server
insertr serve --dev-mode insertr serve --dev-mode
# Developer changes files -> enhancement updates templates automatically # Editor changes -> only html_content updated, templates preserved
# Editor changes content -> templates preserved, only content updated # Style detection uses stored original_template for consistency
# Style detection uses current templates (reflecting latest dev intent)
# Ready for handoff:
insertr templates lock --site-id mysite
``` ```
### **Production Phase:** ### **Production Workflow:**
```bash ```bash
# Production mode (templates locked by default) # Production enhancement (no DB cleanup)
insertr enhance ./mysite --site-id mysite
# Production server
insertr serve insertr serve
# Client editing -> only html_content changes, templates preserved # All editing preserves original developer templates
# Style detection always uses locked original_markup # StyleAware editor gets consistent style detection from stored templates
# Developer styles always available regardless of content changes # Content updates only affect html_content field
# For style updates:
insertr templates edit --site-id mysite --content-id specific-element
``` ```
## 🎯 Key Benefits ## 🎯 Key Benefits
### **For Developers:** ### **For Developers:**
**Rapid iteration** during development with automatic template updates **Efficient Processing**: Only process unprocessed elements, skip already handled ones
**Explicit control** over template locking and updates **Development Convenience**: Optional DB cleanup for fresh iterations
**HTML-first approach** aligns with frontend capabilities **HTML-first approach**: Direct alignment with StyleAware editor capabilities
**Clean schema** without legacy compatibility concerns **Zero Configuration**: Automatic detection and processing of viable elements
### **For Clients:** ### **For Content Editors:**
**Style preservation** - developer styles always available **Style Preservation**: Developer styles always available via original_template
**Rich editing** with full HTML capabilities **Rich Editing**: Full HTML capabilities with formatting toolbar
**Version history** includes both content and template context **Perfect Fidelity**: No lossy conversions, complete attribute preservation
**Design safety** - cannot accidentally break developer styling **Design Safety**: Cannot accidentally break developer styling constraints
### **For System:** ### **For System Architecture:**
**Unified processing** - same HTML used for injection and editing **Simplified Flow**: No markdown conversion complexity
**Clear separation** between content updates and template management **Direct Injection**: HTML content injects directly into static files
**Dev/prod integration** leverages existing mode detection **Clean Separation**: Enhancement stores content, API serves editing
**Self-contained** templates preserved in database **Performance**: Skip already-processed elements for faster builds
## 📋 Implementation Tasks ## 📋 Implementation Tasks
### **Phase 3a Priority Tasks:** ### **Week 1: Database Foundation**
1. **Schema Updates**
- [ ] Update SQLite schema: replace `value` with `html_content`, add `original_template`
- [ ] Update PostgreSQL schema: replace `value` with `html_content`, add `original_template`
- [ ] Update `content.sql` queries to use new fields
- [ ] Regenerate SQLC models
1. **Database Schema Update** 2. **API Models**
- [ ] Update `content` table schema - [ ] Update `ContentItem` struct to use `html_content` and `original_template`
- [ ] Update `content_versions` table schema - [ ] Update request/response structs for new field names
- [ ] Update SQLC queries and models - [ ] Update API handlers to work with new field structure
2. **API Model Updates** ### **Week 2: Enhancer Logic**
- [ ] Update `ContentItem` and `CreateContentRequest` structs 3. **First-Pass Processing**
- [ ] Add `html_content` and `original_markup` fields - [ ] Update enhancer to detect processed elements via `data-content-id`
- [ ] Remove `value` field dependencies - [ ] Update enhancer to store `html_content` and `original_template` on first pass
- [ ] Add development DB cleanup option (`--clean-db` flag)
3. **Enhancement Process Updates** ### **Week 3: Injector Redesign**
- [ ] Update content injection to use `html_content` instead of `value` 4. **HTML-Only Injection**
- [ ] Add template detection and storage logic - [ ] Remove `MarkdownProcessor` and all markdown-related code from injector
- [ ] Implement dev/prod mode template handling - [ ] Update injector to use `html_content` directly via `injectHTMLContent()`
- [ ] Remove type-specific content processing (text, markdown, link)
4. **Template Management Commands** ### **Week 4: Integration Testing**
- [ ] Add `insertr templates` command group 5. **StyleAware Editor Compatibility**
- [ ] Implement `lock`, `edit`, `status` subcommands - [ ] Test API responses work correctly with StyleAware editor
- [ ] Add template validation and editor integration - [ ] Verify `original_template` enables proper style detection
- [ ] Test rich HTML editing and injection end-to-end
5. **Frontend Integration** ## 🚀 Implementation Strategy
- [ ] Update API client to handle new response format
- [ ] Modify style detection to use `original_markup`
- [ ] Test rich HTML content editing and injection
## 🔍 Next Steps ### **Priority Order:**
1. **Database Changes First**: Schema, queries, models - foundation for everything else
2. **Enhancer Updates**: First-pass processing logic and content storage
3. **Injector Simplification**: Remove markdown, use HTML directly
4. **Integration Testing**: Verify StyleAware editor compatibility
Tomorrow we will: ### **Key Implementation Notes:**
1. **Begin database schema implementation** - **No Migration Required**: Fresh schema replacement, no backward compatibility needed
2. **Update SQLC queries and regenerate models** - **Enhancer-Driven**: Content storage happens during enhancement, not via API
3. **Modify API handlers for new content structure** - **HTML-Only**: Eliminate all markdown processing complexity
4. **Test the template lifecycle management** - **StyleAware Alignment**: API response format matches editor expectations exactly
This represents a fundamental shift to **HTML-first content management** while maintaining the zero-configuration philosophy that makes Insertr unique. This represents a fundamental shift to **HTML-first content management** with enhanced developer workflow efficiency while maintaining the zero-configuration philosophy that makes Insertr unique.
--- ---
**Status**: Planning Complete, Ready for Implementation **Status**: Ready for Implementation
**Estimated Effort**: 1-2 days for core implementation **Estimated Effort**: 1 week for core implementation
**Breaking Changes**: Yes (fresh schema, no migration needed) **Breaking Changes**: Yes (fresh schema, enhancer workflow changes)

View File

@@ -221,6 +221,7 @@ func runServe(cmd *cobra.Command, args []string) {
contentRouter.Get("/{id}", contentHandler.GetContent) contentRouter.Get("/{id}", contentHandler.GetContent)
contentRouter.Get("/", contentHandler.GetAllContent) contentRouter.Get("/", contentHandler.GetAllContent)
contentRouter.Post("/", contentHandler.CreateContent) contentRouter.Post("/", contentHandler.CreateContent)
contentRouter.Put("/{id}", contentHandler.UpdateContent)
// Version control endpoints // Version control endpoints
contentRouter.Get("/{id}/versions", contentHandler.GetContentVersions) contentRouter.Get("/{id}/versions", contentHandler.GetContentVersions)

View File

@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Debug Style Detection</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 2rem;
line-height: 1.6;
}
.fancy {
color: #7c3aed;
text-decoration: none;
border-bottom: 2px solid #a855f7;
}
.fancy:hover {
background: #ede9fe;
}
.test-output {
background: #f5f5f5;
padding: 1rem;
border-radius: 8px;
font-family: 'Monaco', 'Courier New', monospace;
white-space: pre-wrap;
max-height: 500px;
overflow-y: auto;
margin-top: 1rem;
}
</style>
</head>
<body>
<h1>🔍 Debug Style Detection</h1>
<p>Testing what styles are detected for Example 2:</p>
<!-- This is Example 2 from the simple demo -->
<p id="example2" class="insertr">Visit our <a class="fancy" href="#about">about page</a> for more info.</p>
<button onclick="debugStyleDetection()">Debug Style Detection</button>
<div id="output" class="test-output">Click the button to see debug output...</div>
<script type="module">
import { styleDetectionEngine } from './lib/src/utils/style-detection.js';
window.debugStyleDetection = function() {
const element = document.getElementById('example2');
const output = document.getElementById('output');
console.log('🔍 Debugging style detection for:', element.innerHTML);
// Run style detection
const result = styleDetectionEngine.detectStylesAndStructure(element);
let debugInfo = '🔍 Style Detection Debug Results\n';
debugInfo += '================================\n\n';
debugInfo += `Element HTML: ${element.innerHTML}\n\n`;
debugInfo += `Detected Styles (${result.styles.size}):\n`;
debugInfo += '-------------------------\n';
for (const [styleId, styleInfo] of result.styles) {
debugInfo += `Style ID: ${styleId}\n`;
debugInfo += ` Name: ${styleInfo.name}\n`;
debugInfo += ` Tag: ${styleInfo.tagName}\n`;
debugInfo += ` Classes: [${styleInfo.classes.join(', ')}]\n`;
debugInfo += ` Attributes: ${JSON.stringify(styleInfo.attributes)}\n\n`;
}
debugInfo += `Content Structure (${result.structure.length} pieces):\n`;
debugInfo += '----------------------------\n';
result.structure.forEach((piece, index) => {
debugInfo += `${index}: ${piece.type}`;
if (piece.type === 'styled') {
debugInfo += ` (styleId: ${piece.styleId})`;
}
if (piece.content) {
debugInfo += ` - "${piece.content}"`;
}
debugInfo += '\n';
});
output.textContent = debugInfo;
console.log(debugInfo);
};
</script>
</body>
</html>

2
go.mod
View File

@@ -7,11 +7,11 @@ require (
github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2 github.com/go-chi/cors v1.2.2
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.32 github.com/mattn/go-sqlite3 v1.14.32
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2 github.com/spf13/viper v1.18.2
github.com/yuin/goldmark v1.7.8
golang.org/x/net v0.43.0 golang.org/x/net v0.43.0
golang.org/x/oauth2 v0.31.0 golang.org/x/oauth2 v0.31.0
) )

6
go.sum
View File

@@ -19,6 +19,10 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -70,8 +74,6 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=

View File

@@ -22,6 +22,21 @@ import (
"github.com/insertr/insertr/internal/engine" "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 // ContentHandler handles all content-related HTTP requests
type ContentHandler struct { type ContentHandler struct {
database *db.Database database *db.Database
@@ -316,7 +331,8 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
content, err = h.database.GetSQLiteQueries().UpsertContent(context.Background(), sqlite.UpsertContentParams{ content, err = h.database.GetSQLiteQueries().UpsertContent(context.Background(), sqlite.UpsertContentParams{
ID: contentID, ID: contentID,
SiteID: siteID, SiteID: siteID,
Value: req.Value, HtmlContent: req.HTMLContent,
OriginalTemplate: toNullString(req.OriginalTemplate),
Type: contentType, Type: contentType,
LastEditedBy: userID, LastEditedBy: userID,
}) })
@@ -324,7 +340,8 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
content, err = h.database.GetPostgreSQLQueries().UpsertContent(context.Background(), postgresql.UpsertContentParams{ content, err = h.database.GetPostgreSQLQueries().UpsertContent(context.Background(), postgresql.UpsertContentParams{
ID: contentID, ID: contentID,
SiteID: siteID, SiteID: siteID,
Value: req.Value, HtmlContent: req.HTMLContent,
OriginalTemplate: toNullString(req.OriginalTemplate),
Type: contentType, Type: contentType,
LastEditedBy: userID, LastEditedBy: userID,
}) })
@@ -363,6 +380,111 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(item) 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} // DeleteContent handles DELETE /api/content/{id}
func (h *ContentHandler) DeleteContent(w http.ResponseWriter, r *http.Request) { func (h *ContentHandler) DeleteContent(w http.ResponseWriter, r *http.Request) {
contentID := chi.URLParam(r, "id") contentID := chi.URLParam(r, "id")
@@ -541,7 +663,7 @@ func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request)
case "sqlite3": case "sqlite3":
sqliteVersion := targetVersion.(sqlite.ContentVersion) sqliteVersion := targetVersion.(sqlite.ContentVersion)
updatedContent, err = h.database.GetSQLiteQueries().UpdateContent(context.Background(), sqlite.UpdateContentParams{ updatedContent, err = h.database.GetSQLiteQueries().UpdateContent(context.Background(), sqlite.UpdateContentParams{
Value: sqliteVersion.Value, HtmlContent: sqliteVersion.HtmlContent,
Type: sqliteVersion.Type, Type: sqliteVersion.Type,
LastEditedBy: userID, LastEditedBy: userID,
ID: contentID, ID: contentID,
@@ -550,7 +672,7 @@ func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request)
case "postgresql": case "postgresql":
pgVersion := targetVersion.(postgresql.ContentVersion) pgVersion := targetVersion.(postgresql.ContentVersion)
updatedContent, err = h.database.GetPostgreSQLQueries().UpdateContent(context.Background(), postgresql.UpdateContentParams{ updatedContent, err = h.database.GetPostgreSQLQueries().UpdateContent(context.Background(), postgresql.UpdateContentParams{
Value: pgVersion.Value, HtmlContent: pgVersion.HtmlContent,
Type: pgVersion.Type, Type: pgVersion.Type,
LastEditedBy: userID, LastEditedBy: userID,
ID: contentID, ID: contentID,
@@ -580,7 +702,8 @@ func (h *ContentHandler) convertToAPIContent(content interface{}) ContentItem {
return ContentItem{ return ContentItem{
ID: c.ID, ID: c.ID,
SiteID: c.SiteID, SiteID: c.SiteID,
Value: c.Value, HTMLContent: c.HtmlContent,
OriginalTemplate: fromNullString(c.OriginalTemplate),
Type: c.Type, Type: c.Type,
CreatedAt: time.Unix(c.CreatedAt, 0), CreatedAt: time.Unix(c.CreatedAt, 0),
UpdatedAt: time.Unix(c.UpdatedAt, 0), UpdatedAt: time.Unix(c.UpdatedAt, 0),
@@ -591,7 +714,8 @@ func (h *ContentHandler) convertToAPIContent(content interface{}) ContentItem {
return ContentItem{ return ContentItem{
ID: c.ID, ID: c.ID,
SiteID: c.SiteID, SiteID: c.SiteID,
Value: c.Value, HTMLContent: c.HtmlContent,
OriginalTemplate: fromNullString(c.OriginalTemplate),
Type: c.Type, Type: c.Type,
CreatedAt: time.Unix(c.CreatedAt, 0), CreatedAt: time.Unix(c.CreatedAt, 0),
UpdatedAt: time.Unix(c.UpdatedAt, 0), UpdatedAt: time.Unix(c.UpdatedAt, 0),
@@ -631,7 +755,8 @@ func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []Cont
VersionID: version.VersionID, VersionID: version.VersionID,
ContentID: version.ContentID, ContentID: version.ContentID,
SiteID: version.SiteID, SiteID: version.SiteID,
Value: version.Value, HTMLContent: version.HtmlContent,
OriginalTemplate: fromNullString(version.OriginalTemplate),
Type: version.Type, Type: version.Type,
CreatedAt: time.Unix(version.CreatedAt, 0), CreatedAt: time.Unix(version.CreatedAt, 0),
CreatedBy: version.CreatedBy, CreatedBy: version.CreatedBy,
@@ -646,7 +771,8 @@ func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []Cont
VersionID: int64(version.VersionID), VersionID: int64(version.VersionID),
ContentID: version.ContentID, ContentID: version.ContentID,
SiteID: version.SiteID, SiteID: version.SiteID,
Value: version.Value, HTMLContent: version.HtmlContent,
OriginalTemplate: fromNullString(version.OriginalTemplate),
Type: version.Type, Type: version.Type,
CreatedAt: time.Unix(version.CreatedAt, 0), CreatedAt: time.Unix(version.CreatedAt, 0),
CreatedBy: version.CreatedBy, CreatedBy: version.CreatedBy,
@@ -664,7 +790,8 @@ func (h *ContentHandler) createContentVersion(content interface{}) error {
return h.database.GetSQLiteQueries().CreateContentVersion(context.Background(), sqlite.CreateContentVersionParams{ return h.database.GetSQLiteQueries().CreateContentVersion(context.Background(), sqlite.CreateContentVersionParams{
ContentID: c.ID, ContentID: c.ID,
SiteID: c.SiteID, SiteID: c.SiteID,
Value: c.Value, HtmlContent: c.HtmlContent,
OriginalTemplate: c.OriginalTemplate,
Type: c.Type, Type: c.Type,
CreatedBy: c.LastEditedBy, CreatedBy: c.LastEditedBy,
}) })
@@ -673,7 +800,8 @@ func (h *ContentHandler) createContentVersion(content interface{}) error {
return h.database.GetPostgreSQLQueries().CreateContentVersion(context.Background(), postgresql.CreateContentVersionParams{ return h.database.GetPostgreSQLQueries().CreateContentVersion(context.Background(), postgresql.CreateContentVersionParams{
ContentID: c.ID, ContentID: c.ID,
SiteID: c.SiteID, SiteID: c.SiteID,
Value: c.Value, HtmlContent: c.HtmlContent,
OriginalTemplate: c.OriginalTemplate,
Type: c.Type, Type: c.Type,
CreatedBy: c.LastEditedBy, CreatedBy: c.LastEditedBy,
}) })

View File

@@ -6,7 +6,8 @@ import "time"
type ContentItem struct { type ContentItem struct {
ID string `json:"id"` ID string `json:"id"`
SiteID string `json:"site_id"` SiteID string `json:"site_id"`
Value string `json:"value"` HTMLContent string `json:"html_content"`
OriginalTemplate string `json:"original_template"`
Type string `json:"type"` Type string `json:"type"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -17,7 +18,8 @@ type ContentVersion struct {
VersionID int64 `json:"version_id"` VersionID int64 `json:"version_id"`
ContentID string `json:"content_id"` ContentID string `json:"content_id"`
SiteID string `json:"site_id"` SiteID string `json:"site_id"`
Value string `json:"value"` HTMLContent string `json:"html_content"`
OriginalTemplate string `json:"original_template"`
Type string `json:"type"` Type string `json:"type"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by"` CreatedBy string `json:"created_by"`
@@ -44,7 +46,8 @@ type ElementContext struct {
type CreateContentRequest struct { type CreateContentRequest struct {
HTMLMarkup string `json:"html_markup"` // HTML markup of the element HTMLMarkup string `json:"html_markup"` // HTML markup of the element
FilePath string `json:"file_path"` // File path for consistent ID generation FilePath string `json:"file_path"` // File path for consistent ID generation
Value string `json:"value"` // Content value HTMLContent string `json:"html_content"` // HTML content value
OriginalTemplate string `json:"original_template"` // Original template markup
Type string `json:"type"` // Content type Type string `json:"type"` // Content type
SiteID string `json:"site_id,omitempty"` // Site identifier SiteID string `json:"site_id,omitempty"` // Site identifier
CreatedBy string `json:"created_by,omitempty"` // User who created the content CreatedBy string `json:"created_by,omitempty"` // User who created the content

View File

@@ -164,3 +164,10 @@ func (c *HTTPClient) GetAllContent(siteID string) (map[string]engine.ContentItem
return result, nil 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" "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 // DatabaseClient implements ContentClient for direct database access
type DatabaseClient struct { type DatabaseClient struct {
db *db.Database db *db.Database
@@ -134,7 +142,8 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) engine.Conten
return engine.ContentItem{ return engine.ContentItem{
ID: c.ID, ID: c.ID,
SiteID: c.SiteID, SiteID: c.SiteID,
Value: c.Value, HTMLContent: c.HtmlContent,
OriginalTemplate: getStringFromNullString(c.OriginalTemplate),
Type: c.Type, Type: c.Type,
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339), UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
} }
@@ -143,7 +152,8 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) engine.Conten
return engine.ContentItem{ return engine.ContentItem{
ID: c.ID, ID: c.ID,
SiteID: c.SiteID, SiteID: c.SiteID,
Value: c.Value, HTMLContent: c.HtmlContent,
OriginalTemplate: getStringFromNullString(c.OriginalTemplate),
Type: c.Type, Type: c.Type,
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339), UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
} }
@@ -171,3 +181,61 @@ func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []eng
} }
return []engine.ContentItem{} // Should never happen 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

@@ -19,14 +19,14 @@ func NewMockClient() *MockClient {
"navbar-logo-2b10ad": { "navbar-logo-2b10ad": {
ID: "navbar-logo-2b10ad", ID: "navbar-logo-2b10ad",
SiteID: "demo", SiteID: "demo",
Value: "Acme Consulting Solutions", HTMLContent: "Acme Consulting Solutions",
Type: "text", Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339),
}, },
"navbar-logo-2b10ad-a44bad": { "navbar-logo-2b10ad-a44bad": {
ID: "navbar-logo-2b10ad-a44bad", ID: "navbar-logo-2b10ad-a44bad",
SiteID: "demo", SiteID: "demo",
Value: "Acme Business Advisors", HTMLContent: "Acme Business Advisors",
Type: "text", Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339),
}, },
@@ -35,21 +35,21 @@ func NewMockClient() *MockClient {
"hero-title-7cfeea": { "hero-title-7cfeea": {
ID: "hero-title-7cfeea", ID: "hero-title-7cfeea",
SiteID: "demo", SiteID: "demo",
Value: "Transform Your Business with Strategic Expertise", HTMLContent: "Transform Your Business with Strategic Expertise",
Type: "text", Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339),
}, },
"hero-lead-e47475": { "hero-lead-e47475": {
ID: "hero-lead-e47475", ID: "hero-lead-e47475",
SiteID: "demo", 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.", 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: "markdown", Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339),
}, },
"hero-link-76c620": { "hero-link-76c620": {
ID: "hero-link-76c620", ID: "hero-link-76c620",
SiteID: "demo", SiteID: "demo",
Value: "Schedule Free Consultation", HTMLContent: "Schedule Free Consultation",
Type: "link", Type: "link",
UpdatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339),
}, },
@@ -58,15 +58,15 @@ func NewMockClient() *MockClient {
"hero-title-c70343": { "hero-title-c70343": {
ID: "hero-title-c70343", ID: "hero-title-c70343",
SiteID: "demo", SiteID: "demo",
Value: "About Our Consulting Expertise", HTMLContent: "About Our Consulting Expertise",
Type: "text", Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339),
}, },
"hero-lead-673026": { "hero-lead-673026": {
ID: "hero-lead-673026", ID: "hero-lead-673026",
SiteID: "demo", SiteID: "demo",
Value: "We're a team of **experienced consultants** dedicated to helping small businesses thrive in today's competitive marketplace through proven strategies.", 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: "markdown", Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339),
}, },
@@ -74,15 +74,15 @@ func NewMockClient() *MockClient {
"services-subtitle-c8927c": { "services-subtitle-c8927c": {
ID: "services-subtitle-c8927c", ID: "services-subtitle-c8927c",
SiteID: "demo", SiteID: "demo",
Value: "Our Story", HTMLContent: "Our Story",
Type: "text", Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339),
}, },
"services-text-0d96da": { "services-text-0d96da": {
ID: "services-text-0d96da", ID: "services-text-0d96da",
SiteID: "demo", 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.", 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: "markdown", Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339),
}, },
@@ -90,7 +90,7 @@ func NewMockClient() *MockClient {
"default": { "default": {
ID: "default", ID: "default",
SiteID: "demo", SiteID: "demo",
Value: "[Enhanced Content]", HTMLContent: "[Enhanced Content]",
Type: "text", Type: "text",
UpdatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339),
}, },
@@ -138,3 +138,22 @@ func (m *MockClient) GetAllContent(siteID string) (map[string]engine.ContentItem
return result, nil 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,25 +1,27 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: content.sql // source: content.sql
package postgresql package postgresql
import ( import (
"context" "context"
"database/sql"
"strings" "strings"
) )
const createContent = `-- name: CreateContent :one const createContent = `-- name: CreateContent :one
INSERT INTO content (id, site_id, value, type, last_edited_by) INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5, $6)
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 CreateContentParams struct { type CreateContentParams struct {
ID string `json:"id"` ID string `json:"id"`
SiteID string `json:"site_id"` SiteID string `json:"site_id"`
Value string `json:"value"` HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"` Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"` LastEditedBy string `json:"last_edited_by"`
} }
@@ -28,7 +30,8 @@ func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (C
row := q.db.QueryRowContext(ctx, createContent, row := q.db.QueryRowContext(ctx, createContent,
arg.ID, arg.ID,
arg.SiteID, arg.SiteID,
arg.Value, arg.HtmlContent,
arg.OriginalTemplate,
arg.Type, arg.Type,
arg.LastEditedBy, arg.LastEditedBy,
) )
@@ -36,7 +39,8 @@ func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (C
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.SiteID, &i.SiteID,
&i.Value, &i.HtmlContent,
&i.OriginalTemplate,
&i.Type, &i.Type,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -45,6 +49,16 @@ func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (C
return i, err 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 const deleteContent = `-- name: DeleteContent :exec
DELETE FROM content DELETE FROM content
WHERE id = $1 AND site_id = $2 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 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 FROM content
WHERE site_id = $1 WHERE site_id = $1
ORDER BY updated_at DESC ORDER BY updated_at DESC
@@ -79,7 +93,8 @@ func (q *Queries) GetAllContent(ctx context.Context, siteID string) ([]Content,
if err := rows.Scan( if err := rows.Scan(
&i.ID, &i.ID,
&i.SiteID, &i.SiteID,
&i.Value, &i.HtmlContent,
&i.OriginalTemplate,
&i.Type, &i.Type,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -99,7 +114,7 @@ func (q *Queries) GetAllContent(ctx context.Context, siteID string) ([]Content,
} }
const getBulkContent = `-- name: GetBulkContent :many 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 FROM content
WHERE site_id = $1 AND id IN ($2) 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( if err := rows.Scan(
&i.ID, &i.ID,
&i.SiteID, &i.SiteID,
&i.Value, &i.HtmlContent,
&i.OriginalTemplate,
&i.Type, &i.Type,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -152,7 +168,7 @@ func (q *Queries) GetBulkContent(ctx context.Context, arg GetBulkContentParams)
} }
const getContent = `-- name: GetContent :one 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 FROM content
WHERE id = $1 AND site_id = $2 WHERE id = $1 AND site_id = $2
` `
@@ -168,7 +184,8 @@ func (q *Queries) GetContent(ctx context.Context, arg GetContentParams) (Content
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.SiteID, &i.SiteID,
&i.Value, &i.HtmlContent,
&i.OriginalTemplate,
&i.Type, &i.Type,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -179,13 +196,13 @@ func (q *Queries) GetContent(ctx context.Context, arg GetContentParams) (Content
const updateContent = `-- name: UpdateContent :one const updateContent = `-- name: UpdateContent :one
UPDATE content 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 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 { type UpdateContentParams struct {
Value string `json:"value"` HtmlContent string `json:"html_content"`
Type string `json:"type"` Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"` LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"` ID string `json:"id"`
@@ -194,7 +211,7 @@ type UpdateContentParams struct {
func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error) { func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error) {
row := q.db.QueryRowContext(ctx, updateContent, row := q.db.QueryRowContext(ctx, updateContent,
arg.Value, arg.HtmlContent,
arg.Type, arg.Type,
arg.LastEditedBy, arg.LastEditedBy,
arg.ID, arg.ID,
@@ -204,7 +221,8 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.SiteID, &i.SiteID,
&i.Value, &i.HtmlContent,
&i.OriginalTemplate,
&i.Type, &i.Type,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -214,19 +232,20 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C
} }
const upsertContent = `-- name: UpsertContent :one const upsertContent = `-- name: UpsertContent :one
INSERT INTO content (id, site_id, value, type, last_edited_by) INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT(id, site_id) DO UPDATE SET ON CONFLICT(id, site_id) DO UPDATE SET
value = EXCLUDED.value, html_content = EXCLUDED.html_content,
type = EXCLUDED.type, type = EXCLUDED.type,
last_edited_by = EXCLUDED.last_edited_by 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 { type UpsertContentParams struct {
ID string `json:"id"` ID string `json:"id"`
SiteID string `json:"site_id"` SiteID string `json:"site_id"`
Value string `json:"value"` HtmlContent string `json:"html_content"`
OriginalTemplate sql.NullString `json:"original_template"`
Type string `json:"type"` Type string `json:"type"`
LastEditedBy string `json:"last_edited_by"` LastEditedBy string `json:"last_edited_by"`
} }
@@ -235,7 +254,8 @@ func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (C
row := q.db.QueryRowContext(ctx, upsertContent, row := q.db.QueryRowContext(ctx, upsertContent,
arg.ID, arg.ID,
arg.SiteID, arg.SiteID,
arg.Value, arg.HtmlContent,
arg.OriginalTemplate,
arg.Type, arg.Type,
arg.LastEditedBy, arg.LastEditedBy,
) )
@@ -243,7 +263,8 @@ func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (C
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.SiteID, &i.SiteID,
&i.Value, &i.HtmlContent,
&i.OriginalTemplate,
&i.Type, &i.Type,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,8 @@
CREATE TABLE content ( CREATE TABLE content (
id TEXT NOT NULL, id TEXT NOT NULL,
site_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, type TEXT NOT NULL,
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL, created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
updated_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, version_id SERIAL PRIMARY KEY,
content_id TEXT NOT NULL, content_id TEXT NOT NULL,
site_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, type TEXT NOT NULL,
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL, created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL created_by TEXT DEFAULT 'system' NOT NULL

View File

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

View File

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

View File

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

View File

@@ -1,39 +1,43 @@
-- name: GetContent :one -- 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 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: GetAllContent :many -- 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 FROM content
WHERE site_id = sqlc.arg(site_id) WHERE site_id = sqlc.arg(site_id)
ORDER BY updated_at DESC; ORDER BY updated_at DESC;
-- name: GetBulkContent :many -- 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 FROM content
WHERE site_id = sqlc.arg(site_id) AND id IN (sqlc.slice('ids')); WHERE site_id = sqlc.arg(site_id) AND id IN (sqlc.slice('ids'));
-- name: CreateContent :one -- name: CreateContent :one
INSERT INTO content (id, site_id, value, type, 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(value), sqlc.arg(type), sqlc.arg(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, 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: UpdateContent :one -- name: UpdateContent :one
UPDATE content 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) 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 -- name: UpsertContent :one
INSERT INTO content (id, site_id, value, type, 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(value), sqlc.arg(type), sqlc.arg(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 ON CONFLICT(id, site_id) DO UPDATE SET
value = EXCLUDED.value, html_content = EXCLUDED.html_content,
type = EXCLUDED.type, type = EXCLUDED.type,
last_edited_by = EXCLUDED.last_edited_by 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 -- name: DeleteContent :exec
DELETE FROM content 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 -- name: CreateContentVersion :exec
INSERT INTO content_versions (content_id, site_id, value, type, 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(value), sqlc.arg(type), sqlc.arg(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 -- 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 FROM content_versions
WHERE content_id = sqlc.arg(content_id) AND site_id = sqlc.arg(site_id) WHERE content_id = sqlc.arg(content_id) AND site_id = sqlc.arg(site_id)
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT sqlc.arg(limit_count); LIMIT sqlc.arg(limit_count);
-- name: GetContentVersion :one -- 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 FROM content_versions
WHERE version_id = sqlc.arg(version_id); WHERE version_id = sqlc.arg(version_id);
-- name: GetAllVersionsForSite :many -- name: GetAllVersionsForSite :many
SELECT SELECT
cv.version_id, cv.content_id, cv.site_id, cv.value, cv.type, cv.created_at, cv.created_by, cv.version_id, cv.content_id, cv.site_id, cv.html_content, cv.original_template, cv.type, cv.created_at, cv.created_by,
c.value as current_value c.html_content as current_html_content
FROM content_versions cv FROM content_versions cv
LEFT JOIN content c ON cv.content_id = c.id AND cv.site_id = c.site_id 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) WHERE cv.site_id = sqlc.arg(site_id)

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,8 @@
CREATE TABLE content ( CREATE TABLE content (
id TEXT NOT NULL, id TEXT NOT NULL,
site_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, type TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_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, version_id INTEGER PRIMARY KEY AUTOINCREMENT,
content_id TEXT NOT NULL, content_id TEXT NOT NULL,
site_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, type TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL created_by TEXT DEFAULT 'system' NOT NULL

View File

@@ -2,8 +2,9 @@
CREATE TABLE IF NOT EXISTS content ( CREATE TABLE IF NOT EXISTS content (
id TEXT NOT NULL, id TEXT NOT NULL,
site_id TEXT NOT NULL, site_id TEXT NOT NULL,
value TEXT NOT NULL, html_content TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')), original_template TEXT,
type TEXT NOT NULL CHECK (type IN ('text', 'link')),
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_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, 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, version_id INTEGER PRIMARY KEY AUTOINCREMENT,
content_id TEXT NOT NULL, content_id TEXT NOT NULL,
site_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, type TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL created_by TEXT DEFAULT 'system' NOT NULL

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package engine
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"github.com/insertr/insertr/internal/db" "github.com/insertr/insertr/internal/db"
@@ -9,6 +10,14 @@ import (
"github.com/insertr/insertr/internal/db/sqlite" "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 // DatabaseClient implements ContentClient interface using the database
type DatabaseClient struct { type DatabaseClient struct {
database *db.Database database *db.Database
@@ -35,7 +44,8 @@ func (c *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err
return &ContentItem{ return &ContentItem{
ID: content.ID, ID: content.ID,
SiteID: content.SiteID, SiteID: content.SiteID,
Value: content.Value, HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type, Type: content.Type,
LastEditedBy: content.LastEditedBy, LastEditedBy: content.LastEditedBy,
}, nil }, nil
@@ -51,7 +61,8 @@ func (c *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err
return &ContentItem{ return &ContentItem{
ID: content.ID, ID: content.ID,
SiteID: content.SiteID, SiteID: content.SiteID,
Value: content.Value, HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type, Type: content.Type,
LastEditedBy: content.LastEditedBy, LastEditedBy: content.LastEditedBy,
}, nil }, nil
@@ -78,7 +89,8 @@ func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
items[content.ID] = ContentItem{ items[content.ID] = ContentItem{
ID: content.ID, ID: content.ID,
SiteID: content.SiteID, SiteID: content.SiteID,
Value: content.Value, HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type, Type: content.Type,
LastEditedBy: content.LastEditedBy, LastEditedBy: content.LastEditedBy,
} }
@@ -99,7 +111,8 @@ func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
items[content.ID] = ContentItem{ items[content.ID] = ContentItem{
ID: content.ID, ID: content.ID,
SiteID: content.SiteID, SiteID: content.SiteID,
Value: content.Value, HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type, Type: content.Type,
LastEditedBy: content.LastEditedBy, LastEditedBy: content.LastEditedBy,
} }
@@ -125,7 +138,8 @@ func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
items[content.ID] = ContentItem{ items[content.ID] = ContentItem{
ID: content.ID, ID: content.ID,
SiteID: content.SiteID, SiteID: content.SiteID,
Value: content.Value, HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type, Type: content.Type,
LastEditedBy: content.LastEditedBy, LastEditedBy: content.LastEditedBy,
} }
@@ -143,7 +157,8 @@ func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
items[content.ID] = ContentItem{ items[content.ID] = ContentItem{
ID: content.ID, ID: content.ID,
SiteID: content.SiteID, SiteID: content.SiteID,
Value: content.Value, HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
Type: content.Type, Type: content.Type,
LastEditedBy: content.LastEditedBy, LastEditedBy: content.LastEditedBy,
} }
@@ -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()) 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 idGenerator *IDGenerator
client ContentClient client ContentClient
authProvider *AuthProvider authProvider *AuthProvider
injector *Injector
} }
// NewContentEngine creates a new content processing engine // NewContentEngine creates a new content processing engine
func NewContentEngine(client ContentClient) *ContentEngine { func NewContentEngine(client ContentClient) *ContentEngine {
authProvider := &AuthProvider{Type: "mock"} // default
return &ContentEngine{ return &ContentEngine{
idGenerator: NewIDGenerator(), idGenerator: NewIDGenerator(),
client: client, 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(), idGenerator: NewIDGenerator(),
client: client, client: client,
authProvider: authProvider, 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 // Add/update content attributes to the node
e.addContentAttributes(elem.Node, id, elem.Type) 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 // 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": case "h1", "h2", "h3", "h4", "h5", "h6":
return "text" return "text"
case "p", "div", "section", "article", "span": case "p", "div", "section", "article", "span":
return "markdown" return "text"
default: default:
return "text" return "text"
} }
@@ -211,28 +229,35 @@ func (e *ContentEngine) injectContent(elements []ProcessedElement, siteID string
if contentItem != nil { if contentItem != nil {
// Inject the content into the element // Inject the content into the element
elem.Content = contentItem.Value elem.Content = contentItem.HTMLContent
e.injectContentIntoNode(elem.Node, contentItem.Value, contentItem.Type)
// Update injector siteID for this operation
e.injector.siteID = siteID
e.injector.injectHTMLContent(elem.Node, contentItem.HTMLContent)
} }
} }
return nil return nil
} }
// injectContentIntoNode injects content value into an HTML node // extractHTMLContent extracts the inner HTML content from a node
func (e *ContentEngine) injectContentIntoNode(node *html.Node, content, contentType string) { func (e *ContentEngine) extractHTMLContent(node *html.Node) string {
// Clear existing text content var content strings.Builder
for child := node.FirstChild; child != nil; {
next := child.NextSibling // Render all child nodes in order to preserve HTML structure
if child.Type == html.TextNode { for child := node.FirstChild; child != nil; child = child.NextSibling {
node.RemoveChild(child) if err := html.Render(&content, child); err == nil {
// All nodes (text and element) rendered in correct order
} }
child = next
} }
// Add new text content return strings.TrimSpace(content.String())
textNode := &html.Node{
Type: html.TextNode,
Data: content,
} }
node.AppendChild(textNode)
// 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 package engine
import ( import (
"crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/google/uuid"
"golang.org/x/net/html" "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) // 3. Build readable prefix (deterministic, no runtime counting)
prefix := g.buildDeterministicPrefix(fileName, tag, primaryClass) prefix := g.buildDeterministicPrefix(fileName, tag, primaryClass)
// 5. Add collision-resistant suffix // 5. Add UUID-based suffix for guaranteed uniqueness
signature := g.createSignature(node, filePath) uuidSuffix := uuid.New().String()[:6] // Use first 6 chars of UUID
hash := sha256.Sum256([]byte(signature))
suffix := hex.EncodeToString(hash[:3])
finalID := fmt.Sprintf("%s-%s", prefix, suffix) finalID := fmt.Sprintf("%s-%s", prefix, uuidSuffix)
// Ensure uniqueness (should be guaranteed by hash, but safety check) // Ensure uniqueness (should be guaranteed by hash, but safety check)
g.usedIDs[finalID] = true g.usedIDs[finalID] = true
@@ -114,14 +111,10 @@ func (g *IDGenerator) buildPrefix(fileName, tag, primaryClass string, index int)
return strings.Join(parts, "-") 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 { func (g *IDGenerator) createSignature(node *html.Node, filePath string) string {
// Minimal signature for uniqueness // This method is kept for compatibility but not used in UUID-based generation
tag := node.Data return ""
classes := strings.Join(GetClasses(node), " ")
domPath := g.getSimpleDOMPath(node)
return fmt.Sprintf("%s|%s|%s|%s", filePath, domPath, tag, classes)
} }
// getSimpleDOMPath creates a simple DOM path for uniqueness // getSimpleDOMPath creates a simple DOM path for uniqueness
@@ -142,3 +135,68 @@ func (g *IDGenerator) getSimpleDOMPath(node *html.Node) string {
return strings.Join(pathParts, ">") 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 { type Injector struct {
client ContentClient client ContentClient
siteID string siteID string
mdProcessor *MarkdownProcessor
authProvider *AuthProvider authProvider *AuthProvider
} }
@@ -21,7 +20,6 @@ func NewInjector(client ContentClient, siteID string) *Injector {
return &Injector{ return &Injector{
client: client, client: client,
siteID: siteID, siteID: siteID,
mdProcessor: NewMarkdownProcessor(),
authProvider: &AuthProvider{Type: "mock"}, // default authProvider: &AuthProvider{Type: "mock"}, // default
} }
} }
@@ -34,7 +32,6 @@ func NewInjectorWithAuth(client ContentClient, siteID string, authProvider *Auth
return &Injector{ return &Injector{
client: client, client: client,
siteID: siteID, siteID: siteID,
mdProcessor: NewMarkdownProcessor(),
authProvider: authProvider, authProvider: authProvider,
} }
} }
@@ -53,17 +50,8 @@ func (i *Injector) InjectContent(element *Element, contentID string) error {
return nil return nil
} }
// Replace element content based on type // Direct HTML injection for all content types
switch element.Type { i.injectHTMLContent(element.Node, contentItem.HTMLContent)
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)
}
// Add data attributes for editor functionality // Add data attributes for editor functionality
i.AddContentAttributes(element.Node, contentID, element.Type) i.AddContentAttributes(element.Node, contentID, element.Type)
@@ -97,65 +85,13 @@ func (i *Injector) InjectBulkContent(elements []ElementWithID) error {
continue continue
} }
// Replace content based on type // Direct HTML injection for all content types
switch elem.Element.Type { i.injectHTMLContent(elem.Element.Node, contentItem.HTMLContent)
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)
}
} }
return nil 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 // injectHTMLContent safely injects HTML content into a DOM node
// Preserves the original element and only replaces its content // Preserves the original element and only replaces its content
func (i *Injector) injectHTMLContent(node *html.Node, htmlContent string) { 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 // Parse HTML string
doc, err := html.Parse(strings.NewReader(wrappedHTML)) doc, err := html.Parse(strings.NewReader(wrappedHTML))
if err != nil { if err != nil {
log.Printf("Failed to parse HTML content '%s': %v, falling back to text", htmlContent, err) log.Printf("Failed to parse HTML content '%s': %v, falling back to text node", htmlContent, err)
i.injectTextContent(node, htmlContent) // Fallback: inject as text node
i.clearNode(node)
textNode := &html.Node{
Type: html.TextNode,
Data: htmlContent,
}
node.AppendChild(textNode)
return 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 { type ProcessedElement struct {
Node *html.Node // HTML node Node *html.Node // HTML node
ID string // Generated content ID ID string // Generated content ID
Type string // Content type (text, markdown, link) Type string // Content type (text, link)
Content string // Injected content (if any) Content string // Injected content (if any)
Generated bool // Whether ID was generated (vs existing) Generated bool // Whether ID was generated (vs existing)
Tag string // Element tag name Tag string // Element tag name
@@ -48,13 +48,15 @@ type ContentClient interface {
GetContent(siteID, contentID string) (*ContentItem, error) GetContent(siteID, contentID string) (*ContentItem, error)
GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error)
GetAllContent(siteID 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 // ContentItem represents a piece of content from the database
type ContentItem struct { type ContentItem struct {
ID string `json:"id"` ID string `json:"id"`
SiteID string `json:"site_id"` SiteID string `json:"site_id"`
Value string `json:"value"` HTMLContent string `json:"html_content"`
OriginalTemplate string `json:"original_template"`
Type string `json:"type"` Type string `json:"type"`
UpdatedAt string `json:"updated_at"` UpdatedAt string `json:"updated_at"`
LastEditedBy string `json:"last_edited_by,omitempty"` LastEditedBy string `json:"last_edited_by,omitempty"`

31
lib/package-lock.json generated
View File

@@ -8,10 +8,6 @@
"name": "@insertr/lib", "name": "@insertr/lib",
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": {
"marked": "^16.2.1",
"turndown": "^7.2.1"
},
"devDependencies": { "devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.0", "@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-terser": "^0.4.0", "@rollup/plugin-terser": "^0.4.0",
@@ -68,12 +64,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@mixmark-io/domino": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
"license": "BSD-2-Clause"
},
"node_modules/@rollup/plugin-node-resolve": { "node_modules/@rollup/plugin-node-resolve": {
"version": "15.3.1", "version": "15.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
@@ -264,18 +254,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/marked": {
"version": "16.2.1",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.2.1.tgz",
"integrity": "sha512-r3UrXED9lMlHF97jJByry90cwrZBBvZmjG1L68oYfuPMW+uDTnuMbyJDymCWwbTE+f+3LhpNDKfpR3a3saFyjA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -434,15 +412,6 @@
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
},
"node_modules/turndown": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.1.tgz",
"integrity": "sha512-7YiPJw6rLClQL3oUKN3KgMaXeJJ2lAyZItclgKDurqnH61so4k4IH/qwmMva0zpuJc/FhRExBBnk7EbeFANlgQ==",
"license": "MIT",
"dependencies": {
"@mixmark-io/domino": "^2.2.0"
}
} }
} }
} }

View File

@@ -33,7 +33,7 @@ export class ApiClient {
try { try {
const payload = { const payload = {
html_markup: htmlMarkup, // Always send HTML markup - server extracts ID or generates new one html_markup: htmlMarkup, // Always send HTML markup - server extracts ID or generates new one
value: content, html_content: content,
type: type, type: type,
file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation
}; };
@@ -66,6 +66,40 @@ export class ApiClient {
} }
} }
async updateContent(contentId, content) {
try {
const payload = {
html_content: content
};
const response = await fetch(`${this.baseUrl}/${contentId}?site_id=${this.siteId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getAuthToken()}`
},
body: JSON.stringify(payload)
});
if (response.ok) {
const result = await response.json();
console.log(`✅ Content updated: ${contentId}`);
return result;
} else {
console.warn(`⚠️ Update failed (${response.status}): ${contentId}`);
return false;
}
} catch (error) {
if (error.name === 'TypeError' && error.message.includes('fetch')) {
console.warn(`🔌 API Server not reachable at ${this.baseUrl}`);
console.warn('💡 Start full-stack development: just dev');
} else {
console.error('Failed to update content:', contentId, error);
}
return false;
}
}
async getContentVersions(contentId) { async getContentVersions(contentId) {
try { try {
const response = await fetch(`${this.baseUrl}/${contentId}/versions?site_id=${this.siteId}`); const response = await fetch(`${this.baseUrl}/${contentId}/versions?site_id=${this.siteId}`);

View File

@@ -134,24 +134,13 @@ export class InsertrCore {
detectContentType(element) { detectContentType(element) {
const tag = element.tagName.toLowerCase(); const tag = element.tagName.toLowerCase();
if (element.classList.contains('insertr-group')) { // Only return database-valid types: 'text' or 'link'
return 'group'; if (tag === 'a' || tag === 'button') {
return 'link';
} }
switch (tag) { // All other elements are text content
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
return 'text'; return 'text';
case 'p':
return 'textarea';
case 'a': case 'button':
return 'link';
case 'div': case 'section':
return 'text';
case 'span':
return 'text';
default:
return 'text';
}
} }
// Get all elements with their metadata, including group elements // Get all elements with their metadata, including group elements