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:
342
SERVER_UPDATE.md
342
SERVER_UPDATE.md
@@ -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 != "" {
|
||||||
if existingContent == nil {
|
return // Skip - already processed
|
||||||
// First time: create with template
|
|
||||||
htmlContent := extractContentHTML(node)
|
|
||||||
createContent(siteID, contentID, htmlContent, currentMarkup, !devMode)
|
|
||||||
} else if devMode && !existingContent.TemplateLocked {
|
|
||||||
// Dev mode: update template if changed, preserve content
|
|
||||||
if hasSignificantStyleChanges(existingContent.OriginalMarkup, currentMarkup) {
|
|
||||||
updateTemplate(siteID, contentID, currentMarkup)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Always inject existing html_content
|
|
||||||
injectHTMLContent(node, existingContent.HTMLContent)
|
// Extract content and template
|
||||||
|
contentID := generateContentID(node, filePath)
|
||||||
|
htmlContent := extractInnerHTML(node) // For editing/injection
|
||||||
|
originalTemplate := extractOuterHTML(node) // For style detection
|
||||||
|
contentType := determineContentType(node)
|
||||||
|
|
||||||
|
// Store in database
|
||||||
|
createContent(siteID, contentID, htmlContent, originalTemplate, contentType)
|
||||||
|
|
||||||
|
// Mark as processed
|
||||||
|
setAttribute(node, "data-content-id", contentID)
|
||||||
|
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
|
|
||||||
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:**
|
#### **API Response Format (Matches Editor Expectations):**
|
||||||
```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)
|
||||||
@@ -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)
|
||||||
|
|||||||
90
debug-style-detection.html
Normal file
90
debug-style-detection.html
Normal 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
2
go.mod
@@ -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
6
go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -314,19 +329,21 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
switch h.database.GetDBType() {
|
switch h.database.GetDBType() {
|
||||||
case "sqlite3":
|
case "sqlite3":
|
||||||
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,
|
||||||
Type: contentType,
|
OriginalTemplate: toNullString(req.OriginalTemplate),
|
||||||
LastEditedBy: userID,
|
Type: contentType,
|
||||||
|
LastEditedBy: userID,
|
||||||
})
|
})
|
||||||
case "postgresql":
|
case "postgresql":
|
||||||
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,
|
||||||
Type: contentType,
|
OriginalTemplate: toNullString(req.OriginalTemplate),
|
||||||
LastEditedBy: userID,
|
Type: contentType,
|
||||||
|
LastEditedBy: userID,
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
|
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
|
||||||
@@ -363,6 +380,111 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(item)
|
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,
|
||||||
@@ -578,24 +700,26 @@ func (h *ContentHandler) convertToAPIContent(content interface{}) ContentItem {
|
|||||||
case "sqlite3":
|
case "sqlite3":
|
||||||
c := content.(sqlite.Content)
|
c := content.(sqlite.Content)
|
||||||
return ContentItem{
|
return ContentItem{
|
||||||
ID: c.ID,
|
ID: c.ID,
|
||||||
SiteID: c.SiteID,
|
SiteID: c.SiteID,
|
||||||
Value: c.Value,
|
HTMLContent: c.HtmlContent,
|
||||||
Type: c.Type,
|
OriginalTemplate: fromNullString(c.OriginalTemplate),
|
||||||
CreatedAt: time.Unix(c.CreatedAt, 0),
|
Type: c.Type,
|
||||||
UpdatedAt: time.Unix(c.UpdatedAt, 0),
|
CreatedAt: time.Unix(c.CreatedAt, 0),
|
||||||
LastEditedBy: c.LastEditedBy,
|
UpdatedAt: time.Unix(c.UpdatedAt, 0),
|
||||||
|
LastEditedBy: c.LastEditedBy,
|
||||||
}
|
}
|
||||||
case "postgresql":
|
case "postgresql":
|
||||||
c := content.(postgresql.Content)
|
c := content.(postgresql.Content)
|
||||||
return ContentItem{
|
return ContentItem{
|
||||||
ID: c.ID,
|
ID: c.ID,
|
||||||
SiteID: c.SiteID,
|
SiteID: c.SiteID,
|
||||||
Value: c.Value,
|
HTMLContent: c.HtmlContent,
|
||||||
Type: c.Type,
|
OriginalTemplate: fromNullString(c.OriginalTemplate),
|
||||||
CreatedAt: time.Unix(c.CreatedAt, 0),
|
Type: c.Type,
|
||||||
UpdatedAt: time.Unix(c.UpdatedAt, 0),
|
CreatedAt: time.Unix(c.CreatedAt, 0),
|
||||||
LastEditedBy: c.LastEditedBy,
|
UpdatedAt: time.Unix(c.UpdatedAt, 0),
|
||||||
|
LastEditedBy: c.LastEditedBy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ContentItem{} // Should never happen
|
return ContentItem{} // Should never happen
|
||||||
@@ -628,13 +752,14 @@ func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []Cont
|
|||||||
versions := make([]ContentVersion, len(list))
|
versions := make([]ContentVersion, len(list))
|
||||||
for i, version := range list {
|
for i, version := range list {
|
||||||
versions[i] = ContentVersion{
|
versions[i] = ContentVersion{
|
||||||
VersionID: version.VersionID,
|
VersionID: version.VersionID,
|
||||||
ContentID: version.ContentID,
|
ContentID: version.ContentID,
|
||||||
SiteID: version.SiteID,
|
SiteID: version.SiteID,
|
||||||
Value: version.Value,
|
HTMLContent: version.HtmlContent,
|
||||||
Type: version.Type,
|
OriginalTemplate: fromNullString(version.OriginalTemplate),
|
||||||
CreatedAt: time.Unix(version.CreatedAt, 0),
|
Type: version.Type,
|
||||||
CreatedBy: version.CreatedBy,
|
CreatedAt: time.Unix(version.CreatedAt, 0),
|
||||||
|
CreatedBy: version.CreatedBy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return versions
|
return versions
|
||||||
@@ -643,13 +768,14 @@ func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []Cont
|
|||||||
versions := make([]ContentVersion, len(list))
|
versions := make([]ContentVersion, len(list))
|
||||||
for i, version := range list {
|
for i, version := range list {
|
||||||
versions[i] = ContentVersion{
|
versions[i] = ContentVersion{
|
||||||
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,
|
||||||
Type: version.Type,
|
OriginalTemplate: fromNullString(version.OriginalTemplate),
|
||||||
CreatedAt: time.Unix(version.CreatedAt, 0),
|
Type: version.Type,
|
||||||
CreatedBy: version.CreatedBy,
|
CreatedAt: time.Unix(version.CreatedAt, 0),
|
||||||
|
CreatedBy: version.CreatedBy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return versions
|
return versions
|
||||||
@@ -662,20 +788,22 @@ func (h *ContentHandler) createContentVersion(content interface{}) error {
|
|||||||
case "sqlite3":
|
case "sqlite3":
|
||||||
c := content.(sqlite.Content)
|
c := content.(sqlite.Content)
|
||||||
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,
|
||||||
Type: c.Type,
|
OriginalTemplate: c.OriginalTemplate,
|
||||||
CreatedBy: c.LastEditedBy,
|
Type: c.Type,
|
||||||
|
CreatedBy: c.LastEditedBy,
|
||||||
})
|
})
|
||||||
case "postgresql":
|
case "postgresql":
|
||||||
c := content.(postgresql.Content)
|
c := content.(postgresql.Content)
|
||||||
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,
|
||||||
Type: c.Type,
|
OriginalTemplate: c.OriginalTemplate,
|
||||||
CreatedBy: c.LastEditedBy,
|
Type: c.Type,
|
||||||
|
CreatedBy: c.LastEditedBy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unsupported database type")
|
return fmt.Errorf("unsupported database type")
|
||||||
|
|||||||
@@ -4,23 +4,25 @@ import "time"
|
|||||||
|
|
||||||
// API request/response models
|
// API request/response models
|
||||||
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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate string `json:"original_template"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Type string `json:"type"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
LastEditedBy string `json:"last_edited_by"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
LastEditedBy string `json:"last_edited_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContentVersion struct {
|
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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate string `json:"original_template"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Type string `json:"type"`
|
||||||
CreatedBy string `json:"created_by"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
CreatedBy string `json:"created_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContentResponse struct {
|
type ContentResponse struct {
|
||||||
@@ -42,12 +44,13 @@ type ElementContext struct {
|
|||||||
|
|
||||||
// Request models
|
// Request models
|
||||||
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
|
||||||
Type string `json:"type"` // Content type
|
OriginalTemplate string `json:"original_template"` // Original template markup
|
||||||
SiteID string `json:"site_id,omitempty"` // Site identifier
|
Type string `json:"type"` // Content type
|
||||||
CreatedBy string `json:"created_by,omitempty"` // User who created the content
|
SiteID string `json:"site_id,omitempty"` // Site identifier
|
||||||
|
CreatedBy string `json:"created_by,omitempty"` // User who created the content
|
||||||
}
|
}
|
||||||
|
|
||||||
type RollbackContentRequest struct {
|
type RollbackContentRequest struct {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -132,20 +140,22 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) engine.Conten
|
|||||||
case "sqlite3":
|
case "sqlite3":
|
||||||
c := content.(sqlite.Content)
|
c := content.(sqlite.Content)
|
||||||
return engine.ContentItem{
|
return engine.ContentItem{
|
||||||
ID: c.ID,
|
ID: c.ID,
|
||||||
SiteID: c.SiteID,
|
SiteID: c.SiteID,
|
||||||
Value: c.Value,
|
HTMLContent: c.HtmlContent,
|
||||||
Type: c.Type,
|
OriginalTemplate: getStringFromNullString(c.OriginalTemplate),
|
||||||
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
|
Type: c.Type,
|
||||||
|
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
case "postgresql":
|
case "postgresql":
|
||||||
c := content.(postgresql.Content)
|
c := content.(postgresql.Content)
|
||||||
return engine.ContentItem{
|
return engine.ContentItem{
|
||||||
ID: c.ID,
|
ID: c.ID,
|
||||||
SiteID: c.SiteID,
|
SiteID: c.SiteID,
|
||||||
Value: c.Value,
|
HTMLContent: c.HtmlContent,
|
||||||
Type: c.Type,
|
OriginalTemplate: getStringFromNullString(c.OriginalTemplate),
|
||||||
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
|
Type: c.Type,
|
||||||
|
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return engine.ContentItem{} // Should never happen
|
return engine.ContentItem{} // Should never happen
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,82 +17,82 @@ func NewMockClient() *MockClient {
|
|||||||
data := map[string]engine.ContentItem{
|
data := map[string]engine.ContentItem{
|
||||||
// Navigation (index.html has collision suffix)
|
// Navigation (index.html has collision suffix)
|
||||||
"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),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Hero Section - index.html (updated with actual IDs)
|
// Hero Section - index.html (updated with actual IDs)
|
||||||
"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),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Hero Section - about.html
|
// Hero Section - about.html
|
||||||
"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),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Services Section
|
// Services Section
|
||||||
"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),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Default fallback for any missing content
|
// Default fallback for any missing content
|
||||||
"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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,34 +1,37 @@
|
|||||||
// 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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate sql.NullString `json:"original_template"`
|
||||||
LastEditedBy string `json:"last_edited_by"`
|
Type string `json:"type"`
|
||||||
|
LastEditedBy string `json:"last_edited_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (Content, error) {
|
func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (Content, error) {
|
||||||
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,28 +232,30 @@ 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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate sql.NullString `json:"original_template"`
|
||||||
LastEditedBy string `json:"last_edited_by"`
|
Type string `json:"type"`
|
||||||
|
LastEditedBy string `json:"last_edited_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) {
|
func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) {
|
||||||
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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
// 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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate sql.NullString `json:"original_template"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
Type string `json:"type"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
LastEditedBy string `json:"last_edited_by"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
LastEditedBy string `json:"last_edited_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContentVersion struct {
|
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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate sql.NullString `json:"original_template"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
Type string `json:"type"`
|
||||||
CreatedBy string `json:"created_by"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
CreatedBy string `json:"created_by"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,23 +11,25 @@ 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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate sql.NullString `json:"original_template"`
|
||||||
CreatedBy string `json:"created_by"`
|
Type string `json:"type"`
|
||||||
|
CreatedBy string `json:"created_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error {
|
func (q *Queries) CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error {
|
||||||
_, 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
|
||||||
@@ -66,14 +68,15 @@ type GetAllVersionsForSiteParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetAllVersionsForSiteRow struct {
|
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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate sql.NullString `json:"original_template"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
Type string `json:"type"`
|
||||||
CreatedBy string `json:"created_by"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
CurrentValue sql.NullString `json:"current_value"`
|
CreatedBy string `json:"created_by"`
|
||||||
|
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,
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,34 +1,37 @@
|
|||||||
// 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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate sql.NullString `json:"original_template"`
|
||||||
LastEditedBy string `json:"last_edited_by"`
|
Type string `json:"type"`
|
||||||
|
LastEditedBy string `json:"last_edited_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (Content, error) {
|
func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (Content, error) {
|
||||||
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,28 +232,30 @@ 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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate sql.NullString `json:"original_template"`
|
||||||
LastEditedBy string `json:"last_edited_by"`
|
Type string `json:"type"`
|
||||||
|
LastEditedBy string `json:"last_edited_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) {
|
func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) {
|
||||||
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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
// 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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate sql.NullString `json:"original_template"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
Type string `json:"type"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
LastEditedBy string `json:"last_edited_by"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
LastEditedBy string `json:"last_edited_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContentVersion struct {
|
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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate sql.NullString `json:"original_template"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
Type string `json:"type"`
|
||||||
CreatedBy string `json:"created_by"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
CreatedBy string `json:"created_by"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,23 +11,25 @@ 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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate sql.NullString `json:"original_template"`
|
||||||
CreatedBy string `json:"created_by"`
|
Type string `json:"type"`
|
||||||
|
CreatedBy string `json:"created_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error {
|
func (q *Queries) CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error {
|
||||||
_, 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
|
||||||
@@ -66,14 +68,15 @@ type GetAllVersionsForSiteParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetAllVersionsForSiteRow struct {
|
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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate sql.NullString `json:"original_template"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
Type string `json:"type"`
|
||||||
CreatedBy string `json:"created_by"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
CurrentValue sql.NullString `json:"current_value"`
|
CreatedBy string `json:"created_by"`
|
||||||
|
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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -33,11 +42,12 @@ func (c *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &ContentItem{
|
return &ContentItem{
|
||||||
ID: content.ID,
|
ID: content.ID,
|
||||||
SiteID: content.SiteID,
|
SiteID: content.SiteID,
|
||||||
Value: content.Value,
|
HTMLContent: content.HtmlContent,
|
||||||
Type: content.Type,
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
||||||
LastEditedBy: content.LastEditedBy,
|
Type: content.Type,
|
||||||
|
LastEditedBy: content.LastEditedBy,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
case "postgresql":
|
case "postgresql":
|
||||||
@@ -49,11 +59,12 @@ func (c *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &ContentItem{
|
return &ContentItem{
|
||||||
ID: content.ID,
|
ID: content.ID,
|
||||||
SiteID: content.SiteID,
|
SiteID: content.SiteID,
|
||||||
Value: content.Value,
|
HTMLContent: content.HtmlContent,
|
||||||
Type: content.Type,
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
||||||
LastEditedBy: content.LastEditedBy,
|
Type: content.Type,
|
||||||
|
LastEditedBy: content.LastEditedBy,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -76,11 +87,12 @@ func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
|
|||||||
items := make(map[string]ContentItem)
|
items := make(map[string]ContentItem)
|
||||||
for _, content := range contents {
|
for _, content := range contents {
|
||||||
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,
|
||||||
Type: content.Type,
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
||||||
LastEditedBy: content.LastEditedBy,
|
Type: content.Type,
|
||||||
|
LastEditedBy: content.LastEditedBy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
@@ -97,11 +109,12 @@ func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
|
|||||||
items := make(map[string]ContentItem)
|
items := make(map[string]ContentItem)
|
||||||
for _, content := range contents {
|
for _, content := range contents {
|
||||||
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,
|
||||||
Type: content.Type,
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
||||||
LastEditedBy: content.LastEditedBy,
|
Type: content.Type,
|
||||||
|
LastEditedBy: content.LastEditedBy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
@@ -123,11 +136,12 @@ func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
|
|||||||
items := make(map[string]ContentItem)
|
items := make(map[string]ContentItem)
|
||||||
for _, content := range contents {
|
for _, content := range contents {
|
||||||
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,
|
||||||
Type: content.Type,
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
||||||
LastEditedBy: content.LastEditedBy,
|
Type: content.Type,
|
||||||
|
LastEditedBy: content.LastEditedBy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
@@ -141,11 +155,12 @@ func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
|
|||||||
items := make(map[string]ContentItem)
|
items := make(map[string]ContentItem)
|
||||||
for _, content := range contents {
|
for _, content := range contents {
|
||||||
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,
|
||||||
Type: content.Type,
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
||||||
LastEditedBy: content.LastEditedBy,
|
Type: content.Type,
|
||||||
|
LastEditedBy: content.LastEditedBy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
@@ -154,3 +169,61 @@ func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
|
|||||||
return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType())
|
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}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
// extractOriginalTemplate extracts the outer HTML of the element (including the element itself)
|
||||||
}
|
func (e *ContentEngine) extractOriginalTemplate(node *html.Node) string {
|
||||||
node.AppendChild(textNode)
|
var buf strings.Builder
|
||||||
|
if err := html.Render(&buf, node); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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,16 +48,18 @@ 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"`
|
||||||
Type string `json:"type"`
|
OriginalTemplate string `json:"original_template"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
Type string `json:"type"`
|
||||||
LastEditedBy string `json:"last_edited_by,omitempty"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
LastEditedBy string `json:"last_edited_by,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContentResponse represents the API response structure
|
// ContentResponse represents the API response structure
|
||||||
|
|||||||
31
lib/package-lock.json
generated
31
lib/package-lock.json
generated
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user