From 2177055c765c6a1d54c9c280fbebc18b2213667c Mon Sep 17 00:00:00 2001 From: Joakim Date: Sat, 20 Sep 2025 16:42:00 +0200 Subject: [PATCH] feat: Complete HTML-first architecture implementation with API integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- SERVER_UPDATE.md | 342 ++++++++++++++----------- cmd/serve.go | 1 + debug-style-detection.html | 90 +++++++ go.mod | 2 +- go.sum | 6 +- internal/api/handlers.go | 228 +++++++++++++---- internal/api/models.go | 43 ++-- internal/content/client.go | 7 + internal/content/database.go | 88 ++++++- internal/content/mock.go | 119 +++++---- internal/db/postgresql/content.sql.go | 87 ++++--- internal/db/postgresql/db.go | 2 +- internal/db/postgresql/models.go | 36 +-- internal/db/postgresql/querier.go | 3 +- internal/db/postgresql/schema.sql | 6 +- internal/db/postgresql/setup.sql | 8 +- internal/db/postgresql/setup.sql.go | 10 +- internal/db/postgresql/versions.sql.go | 56 ++-- internal/db/queries/content.sql | 30 ++- internal/db/queries/versions.sql | 12 +- internal/db/sqlite/content.sql.go | 87 ++++--- internal/db/sqlite/db.go | 2 +- internal/db/sqlite/models.go | 36 +-- internal/db/sqlite/querier.go | 3 +- internal/db/sqlite/schema.sql | 6 +- internal/db/sqlite/setup.sql | 8 +- internal/db/sqlite/setup.sql.go | 10 +- internal/db/sqlite/versions.sql.go | 56 ++-- internal/engine/database_client.go | 133 +++++++--- internal/engine/engine.go | 61 +++-- internal/engine/id_generator.go | 86 ++++++- internal/engine/injector.go | 82 +----- internal/engine/markdown.go | 76 ------ internal/engine/types.go | 16 +- lib/package-lock.json | 31 --- lib/src/core/api-client.js | 36 ++- lib/src/core/insertr.js | 21 +- 37 files changed, 1189 insertions(+), 737 deletions(-) create mode 100644 debug-style-detection.html delete mode 100644 internal/engine/markdown.go diff --git a/SERVER_UPDATE.md b/SERVER_UPDATE.md index 650666d..83ce195 100644 --- a/SERVER_UPDATE.md +++ b/SERVER_UPDATE.md @@ -4,229 +4,263 @@ ### **What We Discovered** 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 -- Rich content editing capabilities -- Template-based style preservation +- Rich content editing capabilities with formatting toolbar +- 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. -### **Core Issues Identified** -1. **Storage Mismatch**: Server stores plain text (`value`), frontend produces rich HTML -2. **Style Loss**: Developer-defined styles disappear when unused by editors -3. **Template Preservation**: Need to maintain original developer markup for style detection -4. **Dual Mode Challenge**: Development iteration vs. production stability requirements +### **Core Requirements Identified** +1. **HTML-First Storage**: Replace `value` with `html_content` field for direct HTML storage +2. **Template Preservation**: Store `original_template` for consistent style detection +3. **Enhancer-First Workflow**: Enhancer stores content on first pass, ignores processed elements +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 +-- SQLite schema CREATE TABLE content ( - id TEXT PRIMARY KEY, + id TEXT NOT NULL, site_id TEXT NOT NULL, - html_content TEXT NOT NULL, -- Rich HTML (for BOTH editing AND injection) - original_markup TEXT, -- Developer template markup (for style detection) - template_locked BOOLEAN DEFAULT FALSE, -- Development vs Production mode + html_content TEXT NOT NULL, -- Rich HTML content (innerHTML) + original_template TEXT, -- Original element markup for style detection (outerHTML) type TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - last_edited_by TEXT NOT NULL, - UNIQUE(site_id, id) + created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, + updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, + last_edited_by TEXT DEFAULT 'system' NOT NULL, + PRIMARY KEY (id, site_id) ); -CREATE TABLE content_versions ( - version_id INTEGER PRIMARY KEY AUTOINCREMENT, - content_id TEXT NOT NULL, +-- PostgreSQL schema +CREATE TABLE content ( + id TEXT NOT NULL, site_id TEXT NOT NULL, - html_content TEXT NOT NULL, -- Version HTML content - original_markup TEXT, -- Template at time of version + html_content TEXT NOT NULL, -- Rich HTML content (innerHTML) + original_template TEXT, -- Original element markup for style detection (outerHTML) type TEXT NOT NULL, - created_at INTEGER NOT NULL, - created_by TEXT NOT NULL + created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL, + updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL, + last_edited_by TEXT DEFAULT 'system' NOT NULL, + PRIMARY KEY (id, site_id) ); ``` **Key Changes:** - ✅ **Removed `value` field** - HTML serves both editing and injection needs -- ✅ **Added `original_markup`** - Preserves developer templates for style detection -- ✅ **Added `template_locked`** - Controls template update behavior -- ✅ **Unified storage** - Same HTML content used for build injection and editing +- ✅ **Added `html_content`** - Direct HTML storage for content editing and injection +- ✅ **Added `original_template`** - Preserves developer templates for StyleAware editor style detection +- ✅ **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):** -- Enhancement **updates templates** when developer markup changes significantly -- API editing **preserves templates**, only updates content -- Supports rapid iteration and template refinement +#### **Unprocessed Element Detection:** +- Elements without `data-content-id` attribute are unprocessed +- Enhancer processes these elements and assigns IDs +- Subsequent enhancer runs skip elements that already have `data-content-id` -#### **Production Mode (template_locked = true):** -- 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:** +#### **Content Storage on First Pass:** ```go -func (e *ContentEngine) processElement(node *html.Node, siteID, contentID string, devMode bool) { - existingContent := getContent(siteID, contentID) - currentMarkup := extractElementHTML(node) - - if existingContent == nil { - // 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) - } +func processElement(node *html.Node, siteID string) { + // Check if already processed + existingID := getAttribute(node, "data-content-id") + if existingID != "" { + return // Skip - already processed } - // 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 -func (h *ContentHandler) CreateContent(req CreateContentRequest) { - // API updates only affect html_content, never original_markup - updateContent(req.SiteID, req.ID, req.HTMLContent) - // Template preservation handled automatically +func (i *Injector) InjectContent(element *Element, contentID string) error { + // Fetch content from database + contentItem, err := i.client.GetContent(i.siteID, contentID) + 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:** -```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); -} -``` +### **4. StyleAware Editor Integration** -#### **Updated API Response:** +#### **API Response Format (Matches Editor Expectations):** ```json { "id": "hero-title-abc123", + "site_id": "mysite", "html_content": "

Welcome to Our Company

", - "original_markup": "

Welcome to Our Company

", - "template_locked": true, - "type": "text" + "original_template": "

Welcome to Our Company

", + "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 Phase:** +### **Enhanced Development Workflow:** ```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 -# Developer changes files -> enhancement updates templates automatically -# Editor changes content -> templates preserved, only content updated -# Style detection uses current templates (reflecting latest dev intent) - -# Ready for handoff: -insertr templates lock --site-id mysite +# Editor changes -> only html_content updated, templates preserved +# Style detection uses stored original_template for consistency ``` -### **Production Phase:** +### **Production Workflow:** ```bash -# Production mode (templates locked by default) +# Production enhancement (no DB cleanup) +insertr enhance ./mysite --site-id mysite + +# Production server insertr serve -# Client editing -> only html_content changes, templates preserved -# Style detection always uses locked original_markup -# Developer styles always available regardless of content changes - -# For style updates: -insertr templates edit --site-id mysite --content-id specific-element +# All editing preserves original developer templates +# StyleAware editor gets consistent style detection from stored templates +# Content updates only affect html_content field ``` ## 🎯 Key Benefits ### **For Developers:** -✅ **Rapid iteration** during development with automatic template updates -✅ **Explicit control** over template locking and updates -✅ **HTML-first approach** aligns with frontend capabilities -✅ **Clean schema** without legacy compatibility concerns +✅ **Efficient Processing**: Only process unprocessed elements, skip already handled ones +✅ **Development Convenience**: Optional DB cleanup for fresh iterations +✅ **HTML-first approach**: Direct alignment with StyleAware editor capabilities +✅ **Zero Configuration**: Automatic detection and processing of viable elements -### **For Clients:** -✅ **Style preservation** - developer styles always available -✅ **Rich editing** with full HTML capabilities -✅ **Version history** includes both content and template context -✅ **Design safety** - cannot accidentally break developer styling +### **For Content Editors:** +✅ **Style Preservation**: Developer styles always available via original_template +✅ **Rich Editing**: Full HTML capabilities with formatting toolbar +✅ **Perfect Fidelity**: No lossy conversions, complete attribute preservation +✅ **Design Safety**: Cannot accidentally break developer styling constraints -### **For System:** -✅ **Unified processing** - same HTML used for injection and editing -✅ **Clear separation** between content updates and template management -✅ **Dev/prod integration** leverages existing mode detection -✅ **Self-contained** templates preserved in database +### **For System Architecture:** +✅ **Simplified Flow**: No markdown conversion complexity +✅ **Direct Injection**: HTML content injects directly into static files +✅ **Clean Separation**: Enhancement stores content, API serves editing +✅ **Performance**: Skip already-processed elements for faster builds ## 📋 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** - - [ ] Update `content` table schema - - [ ] Update `content_versions` table schema - - [ ] Update SQLC queries and models +2. **API Models** + - [ ] Update `ContentItem` struct to use `html_content` and `original_template` + - [ ] Update request/response structs for new field names + - [ ] Update API handlers to work with new field structure -2. **API Model Updates** - - [ ] Update `ContentItem` and `CreateContentRequest` structs - - [ ] Add `html_content` and `original_markup` fields - - [ ] Remove `value` field dependencies +### **Week 2: Enhancer Logic** +3. **First-Pass Processing** + - [ ] Update enhancer to detect processed elements via `data-content-id` + - [ ] Update enhancer to store `html_content` and `original_template` on first pass + - [ ] Add development DB cleanup option (`--clean-db` flag) -3. **Enhancement Process Updates** - - [ ] Update content injection to use `html_content` instead of `value` - - [ ] Add template detection and storage logic - - [ ] Implement dev/prod mode template handling +### **Week 3: Injector Redesign** +4. **HTML-Only Injection** + - [ ] Remove `MarkdownProcessor` and all markdown-related code from injector + - [ ] Update injector to use `html_content` directly via `injectHTMLContent()` + - [ ] Remove type-specific content processing (text, markdown, link) -4. **Template Management Commands** - - [ ] Add `insertr templates` command group - - [ ] Implement `lock`, `edit`, `status` subcommands - - [ ] Add template validation and editor integration +### **Week 4: Integration Testing** +5. **StyleAware Editor Compatibility** + - [ ] Test API responses work correctly with StyleAware editor + - [ ] Verify `original_template` enables proper style detection + - [ ] Test rich HTML editing and injection end-to-end -5. **Frontend Integration** - - [ ] Update API client to handle new response format - - [ ] Modify style detection to use `original_markup` - - [ ] Test rich HTML content editing and injection +## 🚀 Implementation Strategy -## 🔍 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: -1. **Begin database schema implementation** -2. **Update SQLC queries and regenerate models** -3. **Modify API handlers for new content structure** -4. **Test the template lifecycle management** +### **Key Implementation Notes:** +- **No Migration Required**: Fresh schema replacement, no backward compatibility needed +- **Enhancer-Driven**: Content storage happens during enhancement, not via API +- **HTML-Only**: Eliminate all markdown processing complexity +- **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 -**Estimated Effort**: 1-2 days for core implementation -**Breaking Changes**: Yes (fresh schema, no migration needed) \ No newline at end of file +**Status**: Ready for Implementation +**Estimated Effort**: 1 week for core implementation +**Breaking Changes**: Yes (fresh schema, enhancer workflow changes) \ No newline at end of file diff --git a/cmd/serve.go b/cmd/serve.go index d1b0137..c789421 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -221,6 +221,7 @@ func runServe(cmd *cobra.Command, args []string) { contentRouter.Get("/{id}", contentHandler.GetContent) contentRouter.Get("/", contentHandler.GetAllContent) contentRouter.Post("/", contentHandler.CreateContent) + contentRouter.Put("/{id}", contentHandler.UpdateContent) // Version control endpoints contentRouter.Get("/{id}/versions", contentHandler.GetContentVersions) diff --git a/debug-style-detection.html b/debug-style-detection.html new file mode 100644 index 0000000..5dd1b04 --- /dev/null +++ b/debug-style-detection.html @@ -0,0 +1,90 @@ + + + + + + Debug Style Detection + + + +

🔍 Debug Style Detection

+

Testing what styles are detected for Example 2:

+ + +

Visit our about page for more info.

+ + +
Click the button to see debug output...
+ + + + \ No newline at end of file diff --git a/go.mod b/go.mod index f0b22b1..b722750 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,11 @@ require ( github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/cors v1.2.2 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/mattn/go-sqlite3 v1.14.32 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 - github.com/yuin/goldmark v1.7.8 golang.org/x/net v0.43.0 golang.org/x/oauth2 v0.31.0 ) diff --git a/go.sum b/go.sum index 8d9dbca..fc9136c 100644 --- a/go.sum +++ b/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/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/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/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 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/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= diff --git a/internal/api/handlers.go b/internal/api/handlers.go index dd9877c..5789ecd 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -22,6 +22,21 @@ import ( "github.com/insertr/insertr/internal/engine" ) +// Helper functions for sql.NullString conversion +func toNullString(s string) sql.NullString { + if s == "" { + return sql.NullString{Valid: false} + } + return sql.NullString{String: s, Valid: true} +} + +func fromNullString(ns sql.NullString) string { + if ns.Valid { + return ns.String + } + return "" +} + // ContentHandler handles all content-related HTTP requests type ContentHandler struct { database *db.Database @@ -314,19 +329,21 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) { switch h.database.GetDBType() { case "sqlite3": content, err = h.database.GetSQLiteQueries().UpsertContent(context.Background(), sqlite.UpsertContentParams{ - ID: contentID, - SiteID: siteID, - Value: req.Value, - Type: contentType, - LastEditedBy: userID, + ID: contentID, + SiteID: siteID, + HtmlContent: req.HTMLContent, + OriginalTemplate: toNullString(req.OriginalTemplate), + Type: contentType, + LastEditedBy: userID, }) case "postgresql": content, err = h.database.GetPostgreSQLQueries().UpsertContent(context.Background(), postgresql.UpsertContentParams{ - ID: contentID, - SiteID: siteID, - Value: req.Value, - Type: contentType, - LastEditedBy: userID, + ID: contentID, + SiteID: siteID, + HtmlContent: req.HTMLContent, + OriginalTemplate: toNullString(req.OriginalTemplate), + Type: contentType, + LastEditedBy: userID, }) default: http.Error(w, "Unsupported database type", http.StatusInternalServerError) @@ -363,6 +380,111 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(item) } +// UpdateContent handles PUT /api/content/{id} +func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) { + contentID := chi.URLParam(r, "id") + siteID := r.URL.Query().Get("site_id") + + if siteID == "" { + http.Error(w, "site_id parameter is required", http.StatusBadRequest) + return + } + + var req struct { + HTMLContent string `json:"html_content"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + // Extract user from request using authentication service + userInfo, authErr := h.authService.ExtractUserFromRequest(r) + if authErr != nil { + http.Error(w, fmt.Sprintf("Authentication error: %v", authErr), http.StatusUnauthorized) + return + } + userID := userInfo.ID + + // Get existing content for version history + var existingContent interface{} + var err error + + switch h.database.GetDBType() { + case "sqlite3": + existingContent, err = h.database.GetSQLiteQueries().GetContent(context.Background(), sqlite.GetContentParams{ + ID: contentID, + SiteID: siteID, + }) + case "postgresql": + existingContent, err = h.database.GetPostgreSQLQueries().GetContent(context.Background(), postgresql.GetContentParams{ + ID: contentID, + SiteID: siteID, + }) + default: + http.Error(w, "Unsupported database type", http.StatusInternalServerError) + return + } + + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "Content not found", http.StatusNotFound) + return + } + http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) + return + } + + // Archive existing version before update + if err := h.createContentVersion(existingContent); err != nil { + fmt.Printf("Warning: Failed to create content version: %v\n", err) + } + + // Update content + var updatedContent interface{} + + switch h.database.GetDBType() { + case "sqlite3": + updatedContent, err = h.database.GetSQLiteQueries().UpdateContent(context.Background(), sqlite.UpdateContentParams{ + HtmlContent: req.HTMLContent, + Type: h.getContentType(existingContent), + LastEditedBy: userID, + ID: contentID, + SiteID: siteID, + }) + case "postgresql": + updatedContent, err = h.database.GetPostgreSQLQueries().UpdateContent(context.Background(), postgresql.UpdateContentParams{ + HtmlContent: req.HTMLContent, + Type: h.getContentType(existingContent), + LastEditedBy: userID, + ID: contentID, + SiteID: siteID, + }) + default: + http.Error(w, "Unsupported database type", http.StatusInternalServerError) + return + } + + if err != nil { + http.Error(w, fmt.Sprintf("Failed to update content: %v", err), http.StatusInternalServerError) + return + } + + item := h.convertToAPIContent(updatedContent) + + // Trigger file enhancement if site is registered for auto-enhancement + if h.siteManager != nil && h.siteManager.IsAutoEnhanceEnabled(siteID) { + go func() { + if err := h.siteManager.EnhanceSite(siteID); err != nil { + log.Printf("⚠️ Failed to enhance site %s: %v", siteID, err) + } + }() + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(item) +} + // DeleteContent handles DELETE /api/content/{id} func (h *ContentHandler) DeleteContent(w http.ResponseWriter, r *http.Request) { contentID := chi.URLParam(r, "id") @@ -541,7 +663,7 @@ func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request) case "sqlite3": sqliteVersion := targetVersion.(sqlite.ContentVersion) updatedContent, err = h.database.GetSQLiteQueries().UpdateContent(context.Background(), sqlite.UpdateContentParams{ - Value: sqliteVersion.Value, + HtmlContent: sqliteVersion.HtmlContent, Type: sqliteVersion.Type, LastEditedBy: userID, ID: contentID, @@ -550,7 +672,7 @@ func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request) case "postgresql": pgVersion := targetVersion.(postgresql.ContentVersion) updatedContent, err = h.database.GetPostgreSQLQueries().UpdateContent(context.Background(), postgresql.UpdateContentParams{ - Value: pgVersion.Value, + HtmlContent: pgVersion.HtmlContent, Type: pgVersion.Type, LastEditedBy: userID, ID: contentID, @@ -578,24 +700,26 @@ func (h *ContentHandler) convertToAPIContent(content interface{}) ContentItem { case "sqlite3": c := content.(sqlite.Content) return ContentItem{ - ID: c.ID, - SiteID: c.SiteID, - Value: c.Value, - Type: c.Type, - CreatedAt: time.Unix(c.CreatedAt, 0), - UpdatedAt: time.Unix(c.UpdatedAt, 0), - LastEditedBy: c.LastEditedBy, + ID: c.ID, + SiteID: c.SiteID, + HTMLContent: c.HtmlContent, + OriginalTemplate: fromNullString(c.OriginalTemplate), + Type: c.Type, + CreatedAt: time.Unix(c.CreatedAt, 0), + UpdatedAt: time.Unix(c.UpdatedAt, 0), + LastEditedBy: c.LastEditedBy, } case "postgresql": c := content.(postgresql.Content) return ContentItem{ - ID: c.ID, - SiteID: c.SiteID, - Value: c.Value, - Type: c.Type, - CreatedAt: time.Unix(c.CreatedAt, 0), - UpdatedAt: time.Unix(c.UpdatedAt, 0), - LastEditedBy: c.LastEditedBy, + ID: c.ID, + SiteID: c.SiteID, + HTMLContent: c.HtmlContent, + OriginalTemplate: fromNullString(c.OriginalTemplate), + Type: c.Type, + CreatedAt: time.Unix(c.CreatedAt, 0), + UpdatedAt: time.Unix(c.UpdatedAt, 0), + LastEditedBy: c.LastEditedBy, } } return ContentItem{} // Should never happen @@ -628,13 +752,14 @@ func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []Cont versions := make([]ContentVersion, len(list)) for i, version := range list { versions[i] = ContentVersion{ - VersionID: version.VersionID, - ContentID: version.ContentID, - SiteID: version.SiteID, - Value: version.Value, - Type: version.Type, - CreatedAt: time.Unix(version.CreatedAt, 0), - CreatedBy: version.CreatedBy, + VersionID: version.VersionID, + ContentID: version.ContentID, + SiteID: version.SiteID, + HTMLContent: version.HtmlContent, + OriginalTemplate: fromNullString(version.OriginalTemplate), + Type: version.Type, + CreatedAt: time.Unix(version.CreatedAt, 0), + CreatedBy: version.CreatedBy, } } return versions @@ -643,13 +768,14 @@ func (h *ContentHandler) convertToAPIVersionList(versionList interface{}) []Cont versions := make([]ContentVersion, len(list)) for i, version := range list { versions[i] = ContentVersion{ - VersionID: int64(version.VersionID), - ContentID: version.ContentID, - SiteID: version.SiteID, - Value: version.Value, - Type: version.Type, - CreatedAt: time.Unix(version.CreatedAt, 0), - CreatedBy: version.CreatedBy, + VersionID: int64(version.VersionID), + ContentID: version.ContentID, + SiteID: version.SiteID, + HTMLContent: version.HtmlContent, + OriginalTemplate: fromNullString(version.OriginalTemplate), + Type: version.Type, + CreatedAt: time.Unix(version.CreatedAt, 0), + CreatedBy: version.CreatedBy, } } return versions @@ -662,20 +788,22 @@ func (h *ContentHandler) createContentVersion(content interface{}) error { case "sqlite3": c := content.(sqlite.Content) return h.database.GetSQLiteQueries().CreateContentVersion(context.Background(), sqlite.CreateContentVersionParams{ - ContentID: c.ID, - SiteID: c.SiteID, - Value: c.Value, - Type: c.Type, - CreatedBy: c.LastEditedBy, + ContentID: c.ID, + SiteID: c.SiteID, + HtmlContent: c.HtmlContent, + OriginalTemplate: c.OriginalTemplate, + Type: c.Type, + CreatedBy: c.LastEditedBy, }) case "postgresql": c := content.(postgresql.Content) return h.database.GetPostgreSQLQueries().CreateContentVersion(context.Background(), postgresql.CreateContentVersionParams{ - ContentID: c.ID, - SiteID: c.SiteID, - Value: c.Value, - Type: c.Type, - CreatedBy: c.LastEditedBy, + ContentID: c.ID, + SiteID: c.SiteID, + HtmlContent: c.HtmlContent, + OriginalTemplate: c.OriginalTemplate, + Type: c.Type, + CreatedBy: c.LastEditedBy, }) } return fmt.Errorf("unsupported database type") diff --git a/internal/api/models.go b/internal/api/models.go index a0ddf10..043a356 100644 --- a/internal/api/models.go +++ b/internal/api/models.go @@ -4,23 +4,25 @@ import "time" // API request/response models type ContentItem struct { - ID string `json:"id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - LastEditedBy string `json:"last_edited_by"` + ID string `json:"id"` + SiteID string `json:"site_id"` + HTMLContent string `json:"html_content"` + OriginalTemplate string `json:"original_template"` + Type string `json:"type"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LastEditedBy string `json:"last_edited_by"` } type ContentVersion struct { - VersionID int64 `json:"version_id"` - ContentID string `json:"content_id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - CreatedAt time.Time `json:"created_at"` - CreatedBy string `json:"created_by"` + VersionID int64 `json:"version_id"` + ContentID string `json:"content_id"` + SiteID string `json:"site_id"` + HTMLContent string `json:"html_content"` + OriginalTemplate string `json:"original_template"` + Type string `json:"type"` + CreatedAt time.Time `json:"created_at"` + CreatedBy string `json:"created_by"` } type ContentResponse struct { @@ -42,12 +44,13 @@ type ElementContext struct { // Request models type CreateContentRequest struct { - HTMLMarkup string `json:"html_markup"` // HTML markup of the element - FilePath string `json:"file_path"` // File path for consistent ID generation - Value string `json:"value"` // Content value - Type string `json:"type"` // Content type - SiteID string `json:"site_id,omitempty"` // Site identifier - CreatedBy string `json:"created_by,omitempty"` // User who created the content + HTMLMarkup string `json:"html_markup"` // HTML markup of the element + FilePath string `json:"file_path"` // File path for consistent ID generation + HTMLContent string `json:"html_content"` // HTML content value + OriginalTemplate string `json:"original_template"` // Original template markup + Type string `json:"type"` // Content type + SiteID string `json:"site_id,omitempty"` // Site identifier + CreatedBy string `json:"created_by,omitempty"` // User who created the content } type RollbackContentRequest struct { diff --git a/internal/content/client.go b/internal/content/client.go index a44ab59..a4b48c5 100644 --- a/internal/content/client.go +++ b/internal/content/client.go @@ -164,3 +164,10 @@ func (c *HTTPClient) GetAllContent(siteID string) (map[string]engine.ContentItem return result, nil } + +// CreateContent creates a new content item via HTTP API +func (c *HTTPClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, contentType, lastEditedBy string) (*engine.ContentItem, error) { + // For now, HTTPClient CreateContent is not implemented for enhancer use + // This would typically be used in API-driven enhancement scenarios + return nil, fmt.Errorf("CreateContent not implemented for HTTPClient - use DatabaseClient for enhancement") +} diff --git a/internal/content/database.go b/internal/content/database.go index 09b8c51..5015870 100644 --- a/internal/content/database.go +++ b/internal/content/database.go @@ -12,6 +12,14 @@ import ( "github.com/insertr/insertr/internal/engine" ) +// Helper function to convert sql.NullString to string +func getStringFromNullString(ns sql.NullString) string { + if ns.Valid { + return ns.String + } + return "" +} + // DatabaseClient implements ContentClient for direct database access type DatabaseClient struct { db *db.Database @@ -132,20 +140,22 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) engine.Conten case "sqlite3": c := content.(sqlite.Content) return engine.ContentItem{ - ID: c.ID, - SiteID: c.SiteID, - Value: c.Value, - Type: c.Type, - UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339), + ID: c.ID, + SiteID: c.SiteID, + HTMLContent: c.HtmlContent, + OriginalTemplate: getStringFromNullString(c.OriginalTemplate), + Type: c.Type, + UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339), } case "postgresql": c := content.(postgresql.Content) return engine.ContentItem{ - ID: c.ID, - SiteID: c.SiteID, - Value: c.Value, - Type: c.Type, - UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339), + ID: c.ID, + SiteID: c.SiteID, + HTMLContent: c.HtmlContent, + OriginalTemplate: getStringFromNullString(c.OriginalTemplate), + Type: c.Type, + UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339), } } return engine.ContentItem{} // Should never happen @@ -171,3 +181,61 @@ func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []eng } return []engine.ContentItem{} // Should never happen } + +// CreateContent creates a new content item +func (c *DatabaseClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, contentType, lastEditedBy string) (*engine.ContentItem, error) { + switch c.db.GetDBType() { + case "sqlite3": + content, err := c.db.GetSQLiteQueries().CreateContent(context.Background(), sqlite.CreateContentParams{ + ID: contentID, + SiteID: siteID, + HtmlContent: htmlContent, + OriginalTemplate: toNullString(originalTemplate), + Type: contentType, + LastEditedBy: lastEditedBy, + }) + if err != nil { + return nil, err + } + return &engine.ContentItem{ + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + Type: content.Type, + LastEditedBy: content.LastEditedBy, + }, nil + + case "postgresql": + content, err := c.db.GetPostgreSQLQueries().CreateContent(context.Background(), postgresql.CreateContentParams{ + ID: contentID, + SiteID: siteID, + HtmlContent: htmlContent, + OriginalTemplate: toNullString(originalTemplate), + Type: contentType, + LastEditedBy: lastEditedBy, + }) + if err != nil { + return nil, err + } + return &engine.ContentItem{ + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + Type: content.Type, + LastEditedBy: content.LastEditedBy, + }, nil + + default: + return nil, fmt.Errorf("unsupported database type: %s", c.db.GetDBType()) + } +} + +// Helper function to convert string to sql.NullString +func toNullString(s string) sql.NullString { + if s == "" { + return sql.NullString{Valid: false} + } + return sql.NullString{String: s, Valid: true} +} diff --git a/internal/content/mock.go b/internal/content/mock.go index 2d45360..5fb0ebe 100644 --- a/internal/content/mock.go +++ b/internal/content/mock.go @@ -17,82 +17,82 @@ func NewMockClient() *MockClient { data := map[string]engine.ContentItem{ // Navigation (index.html has collision suffix) "navbar-logo-2b10ad": { - ID: "navbar-logo-2b10ad", - SiteID: "demo", - Value: "Acme Consulting Solutions", - Type: "text", - UpdatedAt: time.Now().Format(time.RFC3339), + ID: "navbar-logo-2b10ad", + SiteID: "demo", + HTMLContent: "Acme Consulting Solutions", + Type: "text", + UpdatedAt: time.Now().Format(time.RFC3339), }, "navbar-logo-2b10ad-a44bad": { - ID: "navbar-logo-2b10ad-a44bad", - SiteID: "demo", - Value: "Acme Business Advisors", - Type: "text", - UpdatedAt: time.Now().Format(time.RFC3339), + ID: "navbar-logo-2b10ad-a44bad", + SiteID: "demo", + HTMLContent: "Acme Business Advisors", + Type: "text", + UpdatedAt: time.Now().Format(time.RFC3339), }, // Hero Section - index.html (updated with actual IDs) "hero-title-7cfeea": { - ID: "hero-title-7cfeea", - SiteID: "demo", - Value: "Transform Your Business with Strategic Expertise", - Type: "text", - UpdatedAt: time.Now().Format(time.RFC3339), + ID: "hero-title-7cfeea", + SiteID: "demo", + HTMLContent: "Transform Your Business with Strategic Expertise", + Type: "text", + UpdatedAt: time.Now().Format(time.RFC3339), }, "hero-lead-e47475": { - ID: "hero-lead-e47475", - SiteID: "demo", - Value: "We help **ambitious businesses** grow through strategic planning, process optimization, and digital transformation. Our team brings 20+ years of experience to accelerate your success.", - Type: "markdown", - UpdatedAt: time.Now().Format(time.RFC3339), + ID: "hero-lead-e47475", + SiteID: "demo", + HTMLContent: "We help ambitious businesses grow through strategic planning, process optimization, and digital transformation. Our team brings 20+ years of experience to accelerate your success.", + Type: "text", + UpdatedAt: time.Now().Format(time.RFC3339), }, "hero-link-76c620": { - ID: "hero-link-76c620", - SiteID: "demo", - Value: "Schedule Free Consultation", - Type: "link", - UpdatedAt: time.Now().Format(time.RFC3339), + ID: "hero-link-76c620", + SiteID: "demo", + HTMLContent: "Schedule Free Consultation", + Type: "link", + UpdatedAt: time.Now().Format(time.RFC3339), }, // Hero Section - about.html "hero-title-c70343": { - ID: "hero-title-c70343", - SiteID: "demo", - Value: "About Our Consulting Expertise", - Type: "text", - UpdatedAt: time.Now().Format(time.RFC3339), + ID: "hero-title-c70343", + SiteID: "demo", + HTMLContent: "About Our Consulting Expertise", + Type: "text", + UpdatedAt: time.Now().Format(time.RFC3339), }, "hero-lead-673026": { - ID: "hero-lead-673026", - SiteID: "demo", - Value: "We're a team of **experienced consultants** dedicated to helping small businesses thrive in today's competitive marketplace through proven strategies.", - Type: "markdown", - UpdatedAt: time.Now().Format(time.RFC3339), + ID: "hero-lead-673026", + SiteID: "demo", + HTMLContent: "We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace through proven strategies.", + Type: "text", + UpdatedAt: time.Now().Format(time.RFC3339), }, // Services Section "services-subtitle-c8927c": { - ID: "services-subtitle-c8927c", - SiteID: "demo", - Value: "Our Story", - Type: "text", - UpdatedAt: time.Now().Format(time.RFC3339), + ID: "services-subtitle-c8927c", + SiteID: "demo", + HTMLContent: "Our Story", + Type: "text", + UpdatedAt: time.Now().Format(time.RFC3339), }, "services-text-0d96da": { - ID: "services-text-0d96da", - SiteID: "demo", - Value: "**Founded in 2020**, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.", - Type: "markdown", - UpdatedAt: time.Now().Format(time.RFC3339), + ID: "services-text-0d96da", + SiteID: "demo", + HTMLContent: "Founded in 2020, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.", + Type: "text", + UpdatedAt: time.Now().Format(time.RFC3339), }, // Default fallback for any missing content "default": { - ID: "default", - SiteID: "demo", - Value: "[Enhanced Content]", - Type: "text", - UpdatedAt: time.Now().Format(time.RFC3339), + ID: "default", + SiteID: "demo", + HTMLContent: "[Enhanced Content]", + Type: "text", + UpdatedAt: time.Now().Format(time.RFC3339), }, } @@ -138,3 +138,22 @@ func (m *MockClient) GetAllContent(siteID string) (map[string]engine.ContentItem return result, nil } + +// CreateContent creates a new mock content item +func (m *MockClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, contentType, lastEditedBy string) (*engine.ContentItem, error) { + // For mock client, just create and store the item + item := engine.ContentItem{ + ID: contentID, + SiteID: siteID, + HTMLContent: htmlContent, + OriginalTemplate: originalTemplate, + Type: contentType, + UpdatedAt: time.Now().Format(time.RFC3339), + LastEditedBy: lastEditedBy, + } + + // Store in mock data + m.data[contentID] = item + + return &item, nil +} diff --git a/internal/db/postgresql/content.sql.go b/internal/db/postgresql/content.sql.go index e307674..76e6e99 100644 --- a/internal/db/postgresql/content.sql.go +++ b/internal/db/postgresql/content.sql.go @@ -1,34 +1,37 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: content.sql package postgresql import ( "context" + "database/sql" "strings" ) const createContent = `-- name: CreateContent :one -INSERT INTO content (id, site_id, value, type, last_edited_by) -VALUES ($1, $2, $3, $4, $5) -RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by +INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by ` type CreateContentParams struct { - ID string `json:"id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - LastEditedBy string `json:"last_edited_by"` + ID string `json:"id"` + SiteID string `json:"site_id"` + HtmlContent string `json:"html_content"` + OriginalTemplate sql.NullString `json:"original_template"` + Type string `json:"type"` + LastEditedBy string `json:"last_edited_by"` } func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (Content, error) { row := q.db.QueryRowContext(ctx, createContent, arg.ID, arg.SiteID, - arg.Value, + arg.HtmlContent, + arg.OriginalTemplate, arg.Type, arg.LastEditedBy, ) @@ -36,7 +39,8 @@ func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (C err := row.Scan( &i.ID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.UpdatedAt, @@ -45,6 +49,16 @@ func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (C return i, err } +const deleteAllSiteContent = `-- name: DeleteAllSiteContent :exec +DELETE FROM content +WHERE site_id = $1 +` + +func (q *Queries) DeleteAllSiteContent(ctx context.Context, siteID string) error { + _, err := q.db.ExecContext(ctx, deleteAllSiteContent, siteID) + return err +} + const deleteContent = `-- name: DeleteContent :exec DELETE FROM content WHERE id = $1 AND site_id = $2 @@ -61,7 +75,7 @@ func (q *Queries) DeleteContent(ctx context.Context, arg DeleteContentParams) er } const getAllContent = `-- name: GetAllContent :many -SELECT id, site_id, value, type, created_at, updated_at, last_edited_by +SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by FROM content WHERE site_id = $1 ORDER BY updated_at DESC @@ -79,7 +93,8 @@ func (q *Queries) GetAllContent(ctx context.Context, siteID string) ([]Content, if err := rows.Scan( &i.ID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.UpdatedAt, @@ -99,7 +114,7 @@ func (q *Queries) GetAllContent(ctx context.Context, siteID string) ([]Content, } const getBulkContent = `-- name: GetBulkContent :many -SELECT id, site_id, value, type, created_at, updated_at, last_edited_by +SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by FROM content WHERE site_id = $1 AND id IN ($2) ` @@ -132,7 +147,8 @@ func (q *Queries) GetBulkContent(ctx context.Context, arg GetBulkContentParams) if err := rows.Scan( &i.ID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.UpdatedAt, @@ -152,7 +168,7 @@ func (q *Queries) GetBulkContent(ctx context.Context, arg GetBulkContentParams) } const getContent = `-- name: GetContent :one -SELECT id, site_id, value, type, created_at, updated_at, last_edited_by +SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by FROM content WHERE id = $1 AND site_id = $2 ` @@ -168,7 +184,8 @@ func (q *Queries) GetContent(ctx context.Context, arg GetContentParams) (Content err := row.Scan( &i.ID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.UpdatedAt, @@ -179,13 +196,13 @@ func (q *Queries) GetContent(ctx context.Context, arg GetContentParams) (Content const updateContent = `-- name: UpdateContent :one UPDATE content -SET value = $1, type = $2, last_edited_by = $3 +SET html_content = $1, type = $2, last_edited_by = $3 WHERE id = $4 AND site_id = $5 -RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by +RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by ` type UpdateContentParams struct { - Value string `json:"value"` + HtmlContent string `json:"html_content"` Type string `json:"type"` LastEditedBy string `json:"last_edited_by"` ID string `json:"id"` @@ -194,7 +211,7 @@ type UpdateContentParams struct { func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error) { row := q.db.QueryRowContext(ctx, updateContent, - arg.Value, + arg.HtmlContent, arg.Type, arg.LastEditedBy, arg.ID, @@ -204,7 +221,8 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C err := row.Scan( &i.ID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.UpdatedAt, @@ -214,28 +232,30 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C } const upsertContent = `-- name: UpsertContent :one -INSERT INTO content (id, site_id, value, type, last_edited_by) -VALUES ($1, $2, $3, $4, $5) +INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by) +VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT(id, site_id) DO UPDATE SET - value = EXCLUDED.value, + html_content = EXCLUDED.html_content, type = EXCLUDED.type, last_edited_by = EXCLUDED.last_edited_by -RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by +RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by ` type UpsertContentParams struct { - ID string `json:"id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - LastEditedBy string `json:"last_edited_by"` + ID string `json:"id"` + SiteID string `json:"site_id"` + HtmlContent string `json:"html_content"` + OriginalTemplate sql.NullString `json:"original_template"` + Type string `json:"type"` + LastEditedBy string `json:"last_edited_by"` } func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) { row := q.db.QueryRowContext(ctx, upsertContent, arg.ID, arg.SiteID, - arg.Value, + arg.HtmlContent, + arg.OriginalTemplate, arg.Type, arg.LastEditedBy, ) @@ -243,7 +263,8 @@ func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (C err := row.Scan( &i.ID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.UpdatedAt, diff --git a/internal/db/postgresql/db.go b/internal/db/postgresql/db.go index 9f77c9d..b9e992d 100644 --- a/internal/db/postgresql/db.go +++ b/internal/db/postgresql/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package postgresql diff --git a/internal/db/postgresql/models.go b/internal/db/postgresql/models.go index 7a53776..61be412 100644 --- a/internal/db/postgresql/models.go +++ b/internal/db/postgresql/models.go @@ -1,25 +1,31 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package postgresql +import ( + "database/sql" +) + type Content struct { - ID string `json:"id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - LastEditedBy string `json:"last_edited_by"` + ID string `json:"id"` + SiteID string `json:"site_id"` + HtmlContent string `json:"html_content"` + OriginalTemplate sql.NullString `json:"original_template"` + Type string `json:"type"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + LastEditedBy string `json:"last_edited_by"` } type ContentVersion struct { - VersionID int32 `json:"version_id"` - ContentID string `json:"content_id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - CreatedAt int64 `json:"created_at"` - CreatedBy string `json:"created_by"` + VersionID int32 `json:"version_id"` + ContentID string `json:"content_id"` + SiteID string `json:"site_id"` + HtmlContent string `json:"html_content"` + OriginalTemplate sql.NullString `json:"original_template"` + Type string `json:"type"` + CreatedAt int64 `json:"created_at"` + CreatedBy string `json:"created_by"` } diff --git a/internal/db/postgresql/querier.go b/internal/db/postgresql/querier.go index ff09adc..4114516 100644 --- a/internal/db/postgresql/querier.go +++ b/internal/db/postgresql/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package postgresql @@ -15,6 +15,7 @@ type Querier interface { CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error CreateUpdateFunction(ctx context.Context) error CreateVersionsLookupIndex(ctx context.Context) error + DeleteAllSiteContent(ctx context.Context, siteID string) error DeleteContent(ctx context.Context, arg DeleteContentParams) error DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsParams) error GetAllContent(ctx context.Context, siteID string) ([]Content, error) diff --git a/internal/db/postgresql/schema.sql b/internal/db/postgresql/schema.sql index 503c392..0e0b4ea 100644 --- a/internal/db/postgresql/schema.sql +++ b/internal/db/postgresql/schema.sql @@ -3,7 +3,8 @@ CREATE TABLE content ( id TEXT NOT NULL, site_id TEXT NOT NULL, - value TEXT NOT NULL, + html_content TEXT NOT NULL, + original_template TEXT, type TEXT NOT NULL, created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL, updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL, @@ -16,7 +17,8 @@ CREATE TABLE content_versions ( version_id SERIAL PRIMARY KEY, content_id TEXT NOT NULL, site_id TEXT NOT NULL, - value TEXT NOT NULL, + html_content TEXT NOT NULL, + original_template TEXT, type TEXT NOT NULL, created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL, created_by TEXT DEFAULT 'system' NOT NULL diff --git a/internal/db/postgresql/setup.sql b/internal/db/postgresql/setup.sql index e9dcd2f..4d8ef51 100644 --- a/internal/db/postgresql/setup.sql +++ b/internal/db/postgresql/setup.sql @@ -2,8 +2,9 @@ CREATE TABLE IF NOT EXISTS content ( id TEXT NOT NULL, site_id TEXT NOT NULL, - value TEXT NOT NULL, - type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')), + html_content TEXT NOT NULL, + original_template TEXT, + type TEXT NOT NULL CHECK (type IN ('text', 'link')), created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL, updated_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL, last_edited_by TEXT DEFAULT 'system' NOT NULL, @@ -15,7 +16,8 @@ CREATE TABLE IF NOT EXISTS content_versions ( version_id SERIAL PRIMARY KEY, content_id TEXT NOT NULL, site_id TEXT NOT NULL, - value TEXT NOT NULL, + html_content TEXT NOT NULL, + original_template TEXT, type TEXT NOT NULL, created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL, created_by TEXT DEFAULT 'system' NOT NULL diff --git a/internal/db/postgresql/setup.sql.go b/internal/db/postgresql/setup.sql.go index 030a0e0..0a100e4 100644 --- a/internal/db/postgresql/setup.sql.go +++ b/internal/db/postgresql/setup.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: setup.sql package postgresql @@ -55,8 +55,9 @@ const initializeSchema = `-- name: InitializeSchema :exec CREATE TABLE IF NOT EXISTS content ( id TEXT NOT NULL, site_id TEXT NOT NULL, - value TEXT NOT NULL, - type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')), + html_content TEXT NOT NULL, + original_template TEXT, + type TEXT NOT NULL CHECK (type IN ('text', 'link')), created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL, updated_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL, last_edited_by TEXT DEFAULT 'system' NOT NULL, @@ -74,7 +75,8 @@ CREATE TABLE IF NOT EXISTS content_versions ( version_id SERIAL PRIMARY KEY, content_id TEXT NOT NULL, site_id TEXT NOT NULL, - value TEXT NOT NULL, + html_content TEXT NOT NULL, + original_template TEXT, type TEXT NOT NULL, created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL, created_by TEXT DEFAULT 'system' NOT NULL diff --git a/internal/db/postgresql/versions.sql.go b/internal/db/postgresql/versions.sql.go index 00bd5d3..9d9939e 100644 --- a/internal/db/postgresql/versions.sql.go +++ b/internal/db/postgresql/versions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: versions.sql package postgresql @@ -11,23 +11,25 @@ import ( ) const createContentVersion = `-- name: CreateContentVersion :exec -INSERT INTO content_versions (content_id, site_id, value, type, created_by) -VALUES ($1, $2, $3, $4, $5) +INSERT INTO content_versions (content_id, site_id, html_content, original_template, type, created_by) +VALUES ($1, $2, $3, $4, $5, $6) ` type CreateContentVersionParams struct { - ContentID string `json:"content_id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - CreatedBy string `json:"created_by"` + ContentID string `json:"content_id"` + SiteID string `json:"site_id"` + HtmlContent string `json:"html_content"` + OriginalTemplate sql.NullString `json:"original_template"` + Type string `json:"type"` + CreatedBy string `json:"created_by"` } func (q *Queries) CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error { _, err := q.db.ExecContext(ctx, createContentVersion, arg.ContentID, arg.SiteID, - arg.Value, + arg.HtmlContent, + arg.OriginalTemplate, arg.Type, arg.CreatedBy, ) @@ -51,8 +53,8 @@ func (q *Queries) DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsPa const getAllVersionsForSite = `-- name: GetAllVersionsForSite :many SELECT - cv.version_id, cv.content_id, cv.site_id, cv.value, cv.type, cv.created_at, cv.created_by, - c.value as current_value + cv.version_id, cv.content_id, cv.site_id, cv.html_content, cv.original_template, cv.type, cv.created_at, cv.created_by, + c.html_content as current_html_content FROM content_versions cv LEFT JOIN content c ON cv.content_id = c.id AND cv.site_id = c.site_id WHERE cv.site_id = $1 @@ -66,14 +68,15 @@ type GetAllVersionsForSiteParams struct { } type GetAllVersionsForSiteRow struct { - VersionID int32 `json:"version_id"` - ContentID string `json:"content_id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - CreatedAt int64 `json:"created_at"` - CreatedBy string `json:"created_by"` - CurrentValue sql.NullString `json:"current_value"` + VersionID int32 `json:"version_id"` + ContentID string `json:"content_id"` + SiteID string `json:"site_id"` + HtmlContent string `json:"html_content"` + OriginalTemplate sql.NullString `json:"original_template"` + Type string `json:"type"` + CreatedAt int64 `json:"created_at"` + CreatedBy string `json:"created_by"` + CurrentHtmlContent sql.NullString `json:"current_html_content"` } func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsForSiteParams) ([]GetAllVersionsForSiteRow, error) { @@ -89,11 +92,12 @@ func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsF &i.VersionID, &i.ContentID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.CreatedBy, - &i.CurrentValue, + &i.CurrentHtmlContent, ); err != nil { return nil, err } @@ -109,7 +113,7 @@ func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsF } const getContentVersion = `-- name: GetContentVersion :one -SELECT version_id, content_id, site_id, value, type, created_at, created_by +SELECT version_id, content_id, site_id, html_content, original_template, type, created_at, created_by FROM content_versions WHERE version_id = $1 ` @@ -121,7 +125,8 @@ func (q *Queries) GetContentVersion(ctx context.Context, versionID int32) (Conte &i.VersionID, &i.ContentID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.CreatedBy, @@ -130,7 +135,7 @@ func (q *Queries) GetContentVersion(ctx context.Context, versionID int32) (Conte } const getContentVersionHistory = `-- name: GetContentVersionHistory :many -SELECT version_id, content_id, site_id, value, type, created_at, created_by +SELECT version_id, content_id, site_id, html_content, original_template, type, created_at, created_by FROM content_versions WHERE content_id = $1 AND site_id = $2 ORDER BY created_at DESC @@ -156,7 +161,8 @@ func (q *Queries) GetContentVersionHistory(ctx context.Context, arg GetContentVe &i.VersionID, &i.ContentID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.CreatedBy, diff --git a/internal/db/queries/content.sql b/internal/db/queries/content.sql index 98a7dbf..f25a98e 100644 --- a/internal/db/queries/content.sql +++ b/internal/db/queries/content.sql @@ -1,39 +1,43 @@ -- name: GetContent :one -SELECT id, site_id, value, type, created_at, updated_at, last_edited_by +SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by FROM content WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id); -- name: GetAllContent :many -SELECT id, site_id, value, type, created_at, updated_at, last_edited_by +SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by FROM content WHERE site_id = sqlc.arg(site_id) ORDER BY updated_at DESC; -- name: GetBulkContent :many -SELECT id, site_id, value, type, created_at, updated_at, last_edited_by +SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by FROM content WHERE site_id = sqlc.arg(site_id) AND id IN (sqlc.slice('ids')); -- name: CreateContent :one -INSERT INTO content (id, site_id, value, type, last_edited_by) -VALUES (sqlc.arg(id), sqlc.arg(site_id), sqlc.arg(value), sqlc.arg(type), sqlc.arg(last_edited_by)) -RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by; +INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by) +VALUES (sqlc.arg(id), sqlc.arg(site_id), sqlc.arg(html_content), sqlc.arg(original_template), sqlc.arg(type), sqlc.arg(last_edited_by)) +RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by; -- name: UpdateContent :one UPDATE content -SET value = sqlc.arg(value), type = sqlc.arg(type), last_edited_by = sqlc.arg(last_edited_by) +SET html_content = sqlc.arg(html_content), type = sqlc.arg(type), last_edited_by = sqlc.arg(last_edited_by) WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id) -RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by; +RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by; -- name: UpsertContent :one -INSERT INTO content (id, site_id, value, type, last_edited_by) -VALUES (sqlc.arg(id), sqlc.arg(site_id), sqlc.arg(value), sqlc.arg(type), sqlc.arg(last_edited_by)) +INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by) +VALUES (sqlc.arg(id), sqlc.arg(site_id), sqlc.arg(html_content), sqlc.arg(original_template), sqlc.arg(type), sqlc.arg(last_edited_by)) ON CONFLICT(id, site_id) DO UPDATE SET - value = EXCLUDED.value, + html_content = EXCLUDED.html_content, type = EXCLUDED.type, last_edited_by = EXCLUDED.last_edited_by -RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by; +RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by; -- name: DeleteContent :exec DELETE FROM content -WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id); \ No newline at end of file +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); \ No newline at end of file diff --git a/internal/db/queries/versions.sql b/internal/db/queries/versions.sql index 1339907..2d756ba 100644 --- a/internal/db/queries/versions.sql +++ b/internal/db/queries/versions.sql @@ -1,23 +1,23 @@ -- name: CreateContentVersion :exec -INSERT INTO content_versions (content_id, site_id, value, type, created_by) -VALUES (sqlc.arg(content_id), sqlc.arg(site_id), sqlc.arg(value), sqlc.arg(type), sqlc.arg(created_by)); +INSERT INTO content_versions (content_id, site_id, html_content, original_template, type, created_by) +VALUES (sqlc.arg(content_id), sqlc.arg(site_id), sqlc.arg(html_content), sqlc.arg(original_template), sqlc.arg(type), sqlc.arg(created_by)); -- name: GetContentVersionHistory :many -SELECT version_id, content_id, site_id, value, type, created_at, created_by +SELECT version_id, content_id, site_id, html_content, original_template, type, created_at, created_by FROM content_versions WHERE content_id = sqlc.arg(content_id) AND site_id = sqlc.arg(site_id) ORDER BY created_at DESC LIMIT sqlc.arg(limit_count); -- name: GetContentVersion :one -SELECT version_id, content_id, site_id, value, type, created_at, created_by +SELECT version_id, content_id, site_id, html_content, original_template, type, created_at, created_by FROM content_versions WHERE version_id = sqlc.arg(version_id); -- name: GetAllVersionsForSite :many SELECT - cv.version_id, cv.content_id, cv.site_id, cv.value, cv.type, cv.created_at, cv.created_by, - c.value as current_value + cv.version_id, cv.content_id, cv.site_id, cv.html_content, cv.original_template, cv.type, cv.created_at, cv.created_by, + c.html_content as current_html_content FROM content_versions cv LEFT JOIN content c ON cv.content_id = c.id AND cv.site_id = c.site_id WHERE cv.site_id = sqlc.arg(site_id) diff --git a/internal/db/sqlite/content.sql.go b/internal/db/sqlite/content.sql.go index 20f5261..95f2c4b 100644 --- a/internal/db/sqlite/content.sql.go +++ b/internal/db/sqlite/content.sql.go @@ -1,34 +1,37 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: content.sql package sqlite import ( "context" + "database/sql" "strings" ) const createContent = `-- name: CreateContent :one -INSERT INTO content (id, site_id, value, type, last_edited_by) -VALUES (?1, ?2, ?3, ?4, ?5) -RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by +INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by) +VALUES (?1, ?2, ?3, ?4, ?5, ?6) +RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by ` type CreateContentParams struct { - ID string `json:"id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - LastEditedBy string `json:"last_edited_by"` + ID string `json:"id"` + SiteID string `json:"site_id"` + HtmlContent string `json:"html_content"` + OriginalTemplate sql.NullString `json:"original_template"` + Type string `json:"type"` + LastEditedBy string `json:"last_edited_by"` } func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (Content, error) { row := q.db.QueryRowContext(ctx, createContent, arg.ID, arg.SiteID, - arg.Value, + arg.HtmlContent, + arg.OriginalTemplate, arg.Type, arg.LastEditedBy, ) @@ -36,7 +39,8 @@ func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (C err := row.Scan( &i.ID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.UpdatedAt, @@ -45,6 +49,16 @@ func (q *Queries) CreateContent(ctx context.Context, arg CreateContentParams) (C return i, err } +const deleteAllSiteContent = `-- name: DeleteAllSiteContent :exec +DELETE FROM content +WHERE site_id = ?1 +` + +func (q *Queries) DeleteAllSiteContent(ctx context.Context, siteID string) error { + _, err := q.db.ExecContext(ctx, deleteAllSiteContent, siteID) + return err +} + const deleteContent = `-- name: DeleteContent :exec DELETE FROM content WHERE id = ?1 AND site_id = ?2 @@ -61,7 +75,7 @@ func (q *Queries) DeleteContent(ctx context.Context, arg DeleteContentParams) er } const getAllContent = `-- name: GetAllContent :many -SELECT id, site_id, value, type, created_at, updated_at, last_edited_by +SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by FROM content WHERE site_id = ?1 ORDER BY updated_at DESC @@ -79,7 +93,8 @@ func (q *Queries) GetAllContent(ctx context.Context, siteID string) ([]Content, if err := rows.Scan( &i.ID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.UpdatedAt, @@ -99,7 +114,7 @@ func (q *Queries) GetAllContent(ctx context.Context, siteID string) ([]Content, } const getBulkContent = `-- name: GetBulkContent :many -SELECT id, site_id, value, type, created_at, updated_at, last_edited_by +SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by FROM content WHERE site_id = ?1 AND id IN (/*SLICE:ids*/?) ` @@ -132,7 +147,8 @@ func (q *Queries) GetBulkContent(ctx context.Context, arg GetBulkContentParams) if err := rows.Scan( &i.ID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.UpdatedAt, @@ -152,7 +168,7 @@ func (q *Queries) GetBulkContent(ctx context.Context, arg GetBulkContentParams) } const getContent = `-- name: GetContent :one -SELECT id, site_id, value, type, created_at, updated_at, last_edited_by +SELECT id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by FROM content WHERE id = ?1 AND site_id = ?2 ` @@ -168,7 +184,8 @@ func (q *Queries) GetContent(ctx context.Context, arg GetContentParams) (Content err := row.Scan( &i.ID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.UpdatedAt, @@ -179,13 +196,13 @@ func (q *Queries) GetContent(ctx context.Context, arg GetContentParams) (Content const updateContent = `-- name: UpdateContent :one UPDATE content -SET value = ?1, type = ?2, last_edited_by = ?3 +SET html_content = ?1, type = ?2, last_edited_by = ?3 WHERE id = ?4 AND site_id = ?5 -RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by +RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by ` type UpdateContentParams struct { - Value string `json:"value"` + HtmlContent string `json:"html_content"` Type string `json:"type"` LastEditedBy string `json:"last_edited_by"` ID string `json:"id"` @@ -194,7 +211,7 @@ type UpdateContentParams struct { func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error) { row := q.db.QueryRowContext(ctx, updateContent, - arg.Value, + arg.HtmlContent, arg.Type, arg.LastEditedBy, arg.ID, @@ -204,7 +221,8 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C err := row.Scan( &i.ID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.UpdatedAt, @@ -214,28 +232,30 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C } const upsertContent = `-- name: UpsertContent :one -INSERT INTO content (id, site_id, value, type, last_edited_by) -VALUES (?1, ?2, ?3, ?4, ?5) +INSERT INTO content (id, site_id, html_content, original_template, type, last_edited_by) +VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(id, site_id) DO UPDATE SET - value = EXCLUDED.value, + html_content = EXCLUDED.html_content, type = EXCLUDED.type, last_edited_by = EXCLUDED.last_edited_by -RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by +RETURNING id, site_id, html_content, original_template, type, created_at, updated_at, last_edited_by ` type UpsertContentParams struct { - ID string `json:"id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - LastEditedBy string `json:"last_edited_by"` + ID string `json:"id"` + SiteID string `json:"site_id"` + HtmlContent string `json:"html_content"` + OriginalTemplate sql.NullString `json:"original_template"` + Type string `json:"type"` + LastEditedBy string `json:"last_edited_by"` } func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) { row := q.db.QueryRowContext(ctx, upsertContent, arg.ID, arg.SiteID, - arg.Value, + arg.HtmlContent, + arg.OriginalTemplate, arg.Type, arg.LastEditedBy, ) @@ -243,7 +263,8 @@ func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (C err := row.Scan( &i.ID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.UpdatedAt, diff --git a/internal/db/sqlite/db.go b/internal/db/sqlite/db.go index 5841324..3c39218 100644 --- a/internal/db/sqlite/db.go +++ b/internal/db/sqlite/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package sqlite diff --git a/internal/db/sqlite/models.go b/internal/db/sqlite/models.go index d8e7a1c..48abaf0 100644 --- a/internal/db/sqlite/models.go +++ b/internal/db/sqlite/models.go @@ -1,25 +1,31 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package sqlite +import ( + "database/sql" +) + type Content struct { - ID string `json:"id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - LastEditedBy string `json:"last_edited_by"` + ID string `json:"id"` + SiteID string `json:"site_id"` + HtmlContent string `json:"html_content"` + OriginalTemplate sql.NullString `json:"original_template"` + Type string `json:"type"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + LastEditedBy string `json:"last_edited_by"` } type ContentVersion struct { - VersionID int64 `json:"version_id"` - ContentID string `json:"content_id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - CreatedAt int64 `json:"created_at"` - CreatedBy string `json:"created_by"` + VersionID int64 `json:"version_id"` + ContentID string `json:"content_id"` + SiteID string `json:"site_id"` + HtmlContent string `json:"html_content"` + OriginalTemplate sql.NullString `json:"original_template"` + Type string `json:"type"` + CreatedAt int64 `json:"created_at"` + CreatedBy string `json:"created_by"` } diff --git a/internal/db/sqlite/querier.go b/internal/db/sqlite/querier.go index c246168..e52f069 100644 --- a/internal/db/sqlite/querier.go +++ b/internal/db/sqlite/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package sqlite @@ -11,6 +11,7 @@ import ( type Querier interface { CreateContent(ctx context.Context, arg CreateContentParams) (Content, error) CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error + DeleteAllSiteContent(ctx context.Context, siteID string) error DeleteContent(ctx context.Context, arg DeleteContentParams) error DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsParams) error GetAllContent(ctx context.Context, siteID string) ([]Content, error) diff --git a/internal/db/sqlite/schema.sql b/internal/db/sqlite/schema.sql index 722ca2f..0e545ea 100644 --- a/internal/db/sqlite/schema.sql +++ b/internal/db/sqlite/schema.sql @@ -3,7 +3,8 @@ CREATE TABLE content ( id TEXT NOT NULL, site_id TEXT NOT NULL, - value TEXT NOT NULL, + html_content TEXT NOT NULL, + original_template TEXT, type TEXT NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, @@ -16,7 +17,8 @@ CREATE TABLE content_versions ( version_id INTEGER PRIMARY KEY AUTOINCREMENT, content_id TEXT NOT NULL, site_id TEXT NOT NULL, - value TEXT NOT NULL, + html_content TEXT NOT NULL, + original_template TEXT, type TEXT NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, created_by TEXT DEFAULT 'system' NOT NULL diff --git a/internal/db/sqlite/setup.sql b/internal/db/sqlite/setup.sql index bfe8fcd..c31c9eb 100644 --- a/internal/db/sqlite/setup.sql +++ b/internal/db/sqlite/setup.sql @@ -2,8 +2,9 @@ CREATE TABLE IF NOT EXISTS content ( id TEXT NOT NULL, site_id TEXT NOT NULL, - value TEXT NOT NULL, - type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')), + html_content TEXT NOT NULL, + original_template TEXT, + type TEXT NOT NULL CHECK (type IN ('text', 'link')), created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, last_edited_by TEXT DEFAULT 'system' NOT NULL, @@ -15,7 +16,8 @@ CREATE TABLE IF NOT EXISTS content_versions ( version_id INTEGER PRIMARY KEY AUTOINCREMENT, content_id TEXT NOT NULL, site_id TEXT NOT NULL, - value TEXT NOT NULL, + html_content TEXT NOT NULL, + original_template TEXT, type TEXT NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, created_by TEXT DEFAULT 'system' NOT NULL diff --git a/internal/db/sqlite/setup.sql.go b/internal/db/sqlite/setup.sql.go index 800ef7e..7d9c107 100644 --- a/internal/db/sqlite/setup.sql.go +++ b/internal/db/sqlite/setup.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: setup.sql package sqlite @@ -13,8 +13,9 @@ const initializeSchema = `-- name: InitializeSchema :exec CREATE TABLE IF NOT EXISTS content ( id TEXT NOT NULL, site_id TEXT NOT NULL, - value TEXT NOT NULL, - type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')), + html_content TEXT NOT NULL, + original_template TEXT, + type TEXT NOT NULL CHECK (type IN ('text', 'link')), created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, last_edited_by TEXT DEFAULT 'system' NOT NULL, @@ -32,7 +33,8 @@ CREATE TABLE IF NOT EXISTS content_versions ( version_id INTEGER PRIMARY KEY AUTOINCREMENT, content_id TEXT NOT NULL, site_id TEXT NOT NULL, - value TEXT NOT NULL, + html_content TEXT NOT NULL, + original_template TEXT, type TEXT NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, created_by TEXT DEFAULT 'system' NOT NULL diff --git a/internal/db/sqlite/versions.sql.go b/internal/db/sqlite/versions.sql.go index 8d46807..17c7f8d 100644 --- a/internal/db/sqlite/versions.sql.go +++ b/internal/db/sqlite/versions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: versions.sql package sqlite @@ -11,23 +11,25 @@ import ( ) const createContentVersion = `-- name: CreateContentVersion :exec -INSERT INTO content_versions (content_id, site_id, value, type, created_by) -VALUES (?1, ?2, ?3, ?4, ?5) +INSERT INTO content_versions (content_id, site_id, html_content, original_template, type, created_by) +VALUES (?1, ?2, ?3, ?4, ?5, ?6) ` type CreateContentVersionParams struct { - ContentID string `json:"content_id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - CreatedBy string `json:"created_by"` + ContentID string `json:"content_id"` + SiteID string `json:"site_id"` + HtmlContent string `json:"html_content"` + OriginalTemplate sql.NullString `json:"original_template"` + Type string `json:"type"` + CreatedBy string `json:"created_by"` } func (q *Queries) CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error { _, err := q.db.ExecContext(ctx, createContentVersion, arg.ContentID, arg.SiteID, - arg.Value, + arg.HtmlContent, + arg.OriginalTemplate, arg.Type, arg.CreatedBy, ) @@ -51,8 +53,8 @@ func (q *Queries) DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsPa const getAllVersionsForSite = `-- name: GetAllVersionsForSite :many SELECT - cv.version_id, cv.content_id, cv.site_id, cv.value, cv.type, cv.created_at, cv.created_by, - c.value as current_value + cv.version_id, cv.content_id, cv.site_id, cv.html_content, cv.original_template, cv.type, cv.created_at, cv.created_by, + c.html_content as current_html_content FROM content_versions cv LEFT JOIN content c ON cv.content_id = c.id AND cv.site_id = c.site_id WHERE cv.site_id = ?1 @@ -66,14 +68,15 @@ type GetAllVersionsForSiteParams struct { } type GetAllVersionsForSiteRow struct { - VersionID int64 `json:"version_id"` - ContentID string `json:"content_id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - CreatedAt int64 `json:"created_at"` - CreatedBy string `json:"created_by"` - CurrentValue sql.NullString `json:"current_value"` + VersionID int64 `json:"version_id"` + ContentID string `json:"content_id"` + SiteID string `json:"site_id"` + HtmlContent string `json:"html_content"` + OriginalTemplate sql.NullString `json:"original_template"` + Type string `json:"type"` + CreatedAt int64 `json:"created_at"` + CreatedBy string `json:"created_by"` + CurrentHtmlContent sql.NullString `json:"current_html_content"` } func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsForSiteParams) ([]GetAllVersionsForSiteRow, error) { @@ -89,11 +92,12 @@ func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsF &i.VersionID, &i.ContentID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.CreatedBy, - &i.CurrentValue, + &i.CurrentHtmlContent, ); err != nil { return nil, err } @@ -109,7 +113,7 @@ func (q *Queries) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsF } const getContentVersion = `-- name: GetContentVersion :one -SELECT version_id, content_id, site_id, value, type, created_at, created_by +SELECT version_id, content_id, site_id, html_content, original_template, type, created_at, created_by FROM content_versions WHERE version_id = ?1 ` @@ -121,7 +125,8 @@ func (q *Queries) GetContentVersion(ctx context.Context, versionID int64) (Conte &i.VersionID, &i.ContentID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.CreatedBy, @@ -130,7 +135,7 @@ func (q *Queries) GetContentVersion(ctx context.Context, versionID int64) (Conte } const getContentVersionHistory = `-- name: GetContentVersionHistory :many -SELECT version_id, content_id, site_id, value, type, created_at, created_by +SELECT version_id, content_id, site_id, html_content, original_template, type, created_at, created_by FROM content_versions WHERE content_id = ?1 AND site_id = ?2 ORDER BY created_at DESC @@ -156,7 +161,8 @@ func (q *Queries) GetContentVersionHistory(ctx context.Context, arg GetContentVe &i.VersionID, &i.ContentID, &i.SiteID, - &i.Value, + &i.HtmlContent, + &i.OriginalTemplate, &i.Type, &i.CreatedAt, &i.CreatedBy, diff --git a/internal/engine/database_client.go b/internal/engine/database_client.go index 632d8ef..e5f37f5 100644 --- a/internal/engine/database_client.go +++ b/internal/engine/database_client.go @@ -2,6 +2,7 @@ package engine import ( "context" + "database/sql" "fmt" "github.com/insertr/insertr/internal/db" @@ -9,6 +10,14 @@ import ( "github.com/insertr/insertr/internal/db/sqlite" ) +// Helper function to convert sql.NullString to string +func getStringFromNullString(ns sql.NullString) string { + if ns.Valid { + return ns.String + } + return "" +} + // DatabaseClient implements ContentClient interface using the database type DatabaseClient struct { database *db.Database @@ -33,11 +42,12 @@ func (c *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err return nil, err } return &ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - Value: content.Value, - Type: content.Type, - LastEditedBy: content.LastEditedBy, + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + Type: content.Type, + LastEditedBy: content.LastEditedBy, }, nil case "postgresql": @@ -49,11 +59,12 @@ func (c *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err return nil, err } return &ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - Value: content.Value, - Type: content.Type, - LastEditedBy: content.LastEditedBy, + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + Type: content.Type, + LastEditedBy: content.LastEditedBy, }, nil default: @@ -76,11 +87,12 @@ func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map items := make(map[string]ContentItem) for _, content := range contents { items[content.ID] = ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - Value: content.Value, - Type: content.Type, - LastEditedBy: content.LastEditedBy, + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + Type: content.Type, + LastEditedBy: content.LastEditedBy, } } return items, nil @@ -97,11 +109,12 @@ func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map items := make(map[string]ContentItem) for _, content := range contents { items[content.ID] = ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - Value: content.Value, - Type: content.Type, - LastEditedBy: content.LastEditedBy, + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + Type: content.Type, + LastEditedBy: content.LastEditedBy, } } return items, nil @@ -123,11 +136,12 @@ func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e items := make(map[string]ContentItem) for _, content := range contents { items[content.ID] = ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - Value: content.Value, - Type: content.Type, - LastEditedBy: content.LastEditedBy, + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + Type: content.Type, + LastEditedBy: content.LastEditedBy, } } return items, nil @@ -141,11 +155,12 @@ func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e items := make(map[string]ContentItem) for _, content := range contents { items[content.ID] = ContentItem{ - ID: content.ID, - SiteID: content.SiteID, - Value: content.Value, - Type: content.Type, - LastEditedBy: content.LastEditedBy, + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + Type: content.Type, + LastEditedBy: content.LastEditedBy, } } return items, nil @@ -154,3 +169,61 @@ func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) } } + +// CreateContent creates a new content item +func (c *DatabaseClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, contentType, lastEditedBy string) (*ContentItem, error) { + switch c.database.GetDBType() { + case "sqlite3": + content, err := c.database.GetSQLiteQueries().CreateContent(context.Background(), sqlite.CreateContentParams{ + ID: contentID, + SiteID: siteID, + HtmlContent: htmlContent, + OriginalTemplate: toNullString(originalTemplate), + Type: contentType, + LastEditedBy: lastEditedBy, + }) + if err != nil { + return nil, err + } + return &ContentItem{ + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + Type: content.Type, + LastEditedBy: content.LastEditedBy, + }, nil + + case "postgresql": + content, err := c.database.GetPostgreSQLQueries().CreateContent(context.Background(), postgresql.CreateContentParams{ + ID: contentID, + SiteID: siteID, + HtmlContent: htmlContent, + OriginalTemplate: toNullString(originalTemplate), + Type: contentType, + LastEditedBy: lastEditedBy, + }) + if err != nil { + return nil, err + } + return &ContentItem{ + ID: content.ID, + SiteID: content.SiteID, + HTMLContent: content.HtmlContent, + OriginalTemplate: getStringFromNullString(content.OriginalTemplate), + Type: content.Type, + LastEditedBy: content.LastEditedBy, + }, nil + + default: + return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType()) + } +} + +// Helper function to convert string to sql.NullString +func toNullString(s string) sql.NullString { + if s == "" { + return sql.NullString{Valid: false} + } + return sql.NullString{String: s, Valid: true} +} diff --git a/internal/engine/engine.go b/internal/engine/engine.go index b88d571..383d0a4 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -17,14 +17,17 @@ type ContentEngine struct { idGenerator *IDGenerator client ContentClient authProvider *AuthProvider + injector *Injector } // NewContentEngine creates a new content processing engine func NewContentEngine(client ContentClient) *ContentEngine { + authProvider := &AuthProvider{Type: "mock"} // default return &ContentEngine{ idGenerator: NewIDGenerator(), client: client, - authProvider: &AuthProvider{Type: "mock"}, // default + authProvider: authProvider, + injector: NewInjector(client, ""), // siteID will be set per operation } } @@ -37,6 +40,7 @@ func NewContentEngineWithAuth(client ContentClient, authProvider *AuthProvider) idGenerator: NewIDGenerator(), client: client, authProvider: authProvider, + injector: NewInjectorWithAuth(client, "", authProvider), // siteID will be set per operation } } @@ -84,6 +88,20 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro // Add/update content attributes to the node e.addContentAttributes(elem.Node, id, elem.Type) + + // Store content and template for newly discovered elements (first-pass) + if wasGenerated && (input.Mode == Enhancement || input.Mode == ContentInjection) { + // Extract content and template from the unprocessed element + htmlContent := e.extractHTMLContent(elem.Node) + originalTemplate := e.extractOriginalTemplate(elem.Node) + + // Store in database via content client + _, err := e.client.CreateContent(input.SiteID, id, htmlContent, originalTemplate, elem.Type, "system") + if err != nil { + // Log error but don't fail the enhancement - content just won't be stored + fmt.Printf("⚠️ Failed to store content for %s: %v\n", id, err) + } + } } // 4. Inject content if required by mode @@ -157,7 +175,7 @@ func (e *ContentEngine) determineContentType(node *html.Node) string { case "h1", "h2", "h3", "h4", "h5", "h6": return "text" case "p", "div", "section", "article", "span": - return "markdown" + return "text" default: return "text" } @@ -211,28 +229,35 @@ func (e *ContentEngine) injectContent(elements []ProcessedElement, siteID string if contentItem != nil { // Inject the content into the element - elem.Content = contentItem.Value - e.injectContentIntoNode(elem.Node, contentItem.Value, contentItem.Type) + elem.Content = contentItem.HTMLContent + + // Update injector siteID for this operation + e.injector.siteID = siteID + e.injector.injectHTMLContent(elem.Node, contentItem.HTMLContent) } } return nil } -// injectContentIntoNode injects content value into an HTML node -func (e *ContentEngine) injectContentIntoNode(node *html.Node, content, contentType string) { - // Clear existing text content - for child := node.FirstChild; child != nil; { - next := child.NextSibling - if child.Type == html.TextNode { - node.RemoveChild(child) +// extractHTMLContent extracts the inner HTML content from a node +func (e *ContentEngine) extractHTMLContent(node *html.Node) string { + var content strings.Builder + + // Render all child nodes in order to preserve HTML structure + for child := node.FirstChild; child != nil; child = child.NextSibling { + if err := html.Render(&content, child); err == nil { + // All nodes (text and element) rendered in correct order } - child = next } - // Add new text content - textNode := &html.Node{ - Type: html.TextNode, - Data: content, - } - node.AppendChild(textNode) + return strings.TrimSpace(content.String()) +} + +// extractOriginalTemplate extracts the outer HTML of the element (including the element itself) +func (e *ContentEngine) extractOriginalTemplate(node *html.Node) string { + var buf strings.Builder + if err := html.Render(&buf, node); err != nil { + return "" + } + return buf.String() } diff --git a/internal/engine/id_generator.go b/internal/engine/id_generator.go index 34d8400..a656ade 100644 --- a/internal/engine/id_generator.go +++ b/internal/engine/id_generator.go @@ -1,12 +1,11 @@ package engine import ( - "crypto/sha256" - "encoding/hex" "fmt" "path/filepath" "strings" + "github.com/google/uuid" "golang.org/x/net/html" ) @@ -36,12 +35,10 @@ func (g *IDGenerator) Generate(node *html.Node, filePath string) string { // 3. Build readable prefix (deterministic, no runtime counting) prefix := g.buildDeterministicPrefix(fileName, tag, primaryClass) - // 5. Add collision-resistant suffix - signature := g.createSignature(node, filePath) - hash := sha256.Sum256([]byte(signature)) - suffix := hex.EncodeToString(hash[:3]) + // 5. Add UUID-based suffix for guaranteed uniqueness + uuidSuffix := uuid.New().String()[:6] // Use first 6 chars of UUID - finalID := fmt.Sprintf("%s-%s", prefix, suffix) + finalID := fmt.Sprintf("%s-%s", prefix, uuidSuffix) // Ensure uniqueness (should be guaranteed by hash, but safety check) g.usedIDs[finalID] = true @@ -114,14 +111,10 @@ func (g *IDGenerator) buildPrefix(fileName, tag, primaryClass string, index int) return strings.Join(parts, "-") } -// createSignature creates a unique signature for collision resistance +// createSignature creates a unique signature for collision resistance (DEPRECATED - using UUID now) func (g *IDGenerator) createSignature(node *html.Node, filePath string) string { - // Minimal signature for uniqueness - tag := node.Data - classes := strings.Join(GetClasses(node), " ") - domPath := g.getSimpleDOMPath(node) - - return fmt.Sprintf("%s|%s|%s|%s", filePath, domPath, tag, classes) + // This method is kept for compatibility but not used in UUID-based generation + return "" } // getSimpleDOMPath creates a simple DOM path for uniqueness @@ -142,3 +135,68 @@ func (g *IDGenerator) getSimpleDOMPath(node *html.Node) string { return strings.Join(pathParts, ">") } + +// getContentPreview extracts first 50 characters of text content for uniqueness +func (g *IDGenerator) getContentPreview(node *html.Node) string { + var text strings.Builder + g.extractTextContent(node, &text) + content := strings.TrimSpace(text.String()) + if len(content) > 50 { + content = content[:50] + } + // Remove newlines and normalize whitespace + content = strings.ReplaceAll(content, "\n", " ") + content = strings.ReplaceAll(content, "\t", " ") + for strings.Contains(content, " ") { + content = strings.ReplaceAll(content, " ", " ") + } + return content +} + +// extractTextContent recursively extracts text content from a node +func (g *IDGenerator) extractTextContent(node *html.Node, text *strings.Builder) { + if node.Type == html.TextNode { + text.WriteString(node.Data) + } + for child := node.FirstChild; child != nil; child = child.NextSibling { + g.extractTextContent(child, text) + } +} + +// getSiblingIndex returns the position of this element among its siblings of the same type +func (g *IDGenerator) getSiblingIndex(node *html.Node) int { + if node.Parent == nil { + return 0 + } + + index := 0 + tag := node.Data + classes := GetClasses(node) + + for sibling := node.Parent.FirstChild; sibling != nil; sibling = sibling.NextSibling { + if sibling.Type == html.ElementNode && sibling.Data == tag { + siblingClasses := GetClasses(sibling) + // Check if classes match (for more precise positioning) + if g.classesMatch(classes, siblingClasses) { + if sibling == node { + return index + } + index++ + } + } + } + return index +} + +// classesMatch checks if two class lists are equivalent +func (g *IDGenerator) classesMatch(classes1, classes2 []string) bool { + if len(classes1) != len(classes2) { + return false + } + for i, class := range classes1 { + if i >= len(classes2) || class != classes2[i] { + return false + } + } + return true +} diff --git a/internal/engine/injector.go b/internal/engine/injector.go index 6c9b9e0..ad30cea 100644 --- a/internal/engine/injector.go +++ b/internal/engine/injector.go @@ -12,7 +12,6 @@ import ( type Injector struct { client ContentClient siteID string - mdProcessor *MarkdownProcessor authProvider *AuthProvider } @@ -21,7 +20,6 @@ func NewInjector(client ContentClient, siteID string) *Injector { return &Injector{ client: client, siteID: siteID, - mdProcessor: NewMarkdownProcessor(), authProvider: &AuthProvider{Type: "mock"}, // default } } @@ -34,7 +32,6 @@ func NewInjectorWithAuth(client ContentClient, siteID string, authProvider *Auth return &Injector{ client: client, siteID: siteID, - mdProcessor: NewMarkdownProcessor(), authProvider: authProvider, } } @@ -53,17 +50,8 @@ func (i *Injector) InjectContent(element *Element, contentID string) error { return nil } - // Replace element content based on type - switch element.Type { - case "text": - i.injectTextContent(element.Node, contentItem.Value) - case "markdown": - i.injectMarkdownContent(element.Node, contentItem.Value) - case "link": - i.injectLinkContent(element.Node, contentItem.Value) - default: - i.injectTextContent(element.Node, contentItem.Value) - } + // Direct HTML injection for all content types + i.injectHTMLContent(element.Node, contentItem.HTMLContent) // Add data attributes for editor functionality i.AddContentAttributes(element.Node, contentID, element.Type) @@ -97,65 +85,13 @@ func (i *Injector) InjectBulkContent(elements []ElementWithID) error { continue } - // Replace content based on type - switch elem.Element.Type { - case "text": - i.injectTextContent(elem.Element.Node, contentItem.Value) - case "markdown": - i.injectMarkdownContent(elem.Element.Node, contentItem.Value) - case "link": - i.injectLinkContent(elem.Element.Node, contentItem.Value) - default: - i.injectTextContent(elem.Element.Node, contentItem.Value) - } + // Direct HTML injection for all content types + i.injectHTMLContent(elem.Element.Node, contentItem.HTMLContent) } return nil } -// injectTextContent replaces text content in an element -func (i *Injector) injectTextContent(node *html.Node, content string) { - // Remove all child nodes - for child := node.FirstChild; child != nil; { - next := child.NextSibling - node.RemoveChild(child) - child = next - } - - // Add new text content - textNode := &html.Node{ - Type: html.TextNode, - Data: content, - } - node.AppendChild(textNode) -} - -// injectMarkdownContent handles markdown content - converts markdown to HTML -func (i *Injector) injectMarkdownContent(node *html.Node, content string) { - if content == "" { - i.injectTextContent(node, "") - return - } - - // Convert markdown to HTML using server processor - htmlContent, err := i.mdProcessor.ToHTML(content) - if err != nil { - log.Printf("⚠️ Markdown conversion failed for content '%s': %v, falling back to text", content, err) - i.injectTextContent(node, content) - return - } - - // Inject the HTML content - i.injectHTMLContent(node, htmlContent) -} - -// injectLinkContent handles link/button content with URL extraction -func (i *Injector) injectLinkContent(node *html.Node, content string) { - // For now, just inject the text content - // TODO: Parse content for URL and text components - i.injectTextContent(node, content) -} - // injectHTMLContent safely injects HTML content into a DOM node // Preserves the original element and only replaces its content func (i *Injector) injectHTMLContent(node *html.Node, htmlContent string) { @@ -172,8 +108,14 @@ func (i *Injector) injectHTMLContent(node *html.Node, htmlContent string) { // Parse HTML string doc, err := html.Parse(strings.NewReader(wrappedHTML)) if err != nil { - log.Printf("Failed to parse HTML content '%s': %v, falling back to text", htmlContent, err) - i.injectTextContent(node, htmlContent) + log.Printf("Failed to parse HTML content '%s': %v, falling back to text node", htmlContent, err) + // Fallback: inject as text node + i.clearNode(node) + textNode := &html.Node{ + Type: html.TextNode, + Data: htmlContent, + } + node.AppendChild(textNode) return } diff --git a/internal/engine/markdown.go b/internal/engine/markdown.go deleted file mode 100644 index 273b16a..0000000 --- a/internal/engine/markdown.go +++ /dev/null @@ -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(), //
instead of
- html.WithHardWraps(), // Line breaks become
- 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

tag, extract just the inner content - html = strings.TrimSpace(html) - - if strings.HasPrefix(html, "

") && strings.HasSuffix(html, "

") { - // Check if this is a single paragraph (no other

tags inside) - inner := html[3 : len(html)-4] // Remove

and

- if !strings.Contains(inner, "

") { - // 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 -} diff --git a/internal/engine/types.go b/internal/engine/types.go index 6530c4d..e1ed683 100644 --- a/internal/engine/types.go +++ b/internal/engine/types.go @@ -35,7 +35,7 @@ type ContentResult struct { type ProcessedElement struct { Node *html.Node // HTML node ID string // Generated content ID - Type string // Content type (text, markdown, link) + Type string // Content type (text, link) Content string // Injected content (if any) Generated bool // Whether ID was generated (vs existing) Tag string // Element tag name @@ -48,16 +48,18 @@ type ContentClient interface { GetContent(siteID, contentID string) (*ContentItem, error) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) GetAllContent(siteID string) (map[string]ContentItem, error) + CreateContent(siteID, contentID, htmlContent, originalTemplate, contentType, lastEditedBy string) (*ContentItem, error) } // ContentItem represents a piece of content from the database type ContentItem struct { - ID string `json:"id"` - SiteID string `json:"site_id"` - Value string `json:"value"` - Type string `json:"type"` - UpdatedAt string `json:"updated_at"` - LastEditedBy string `json:"last_edited_by,omitempty"` + ID string `json:"id"` + SiteID string `json:"site_id"` + HTMLContent string `json:"html_content"` + OriginalTemplate string `json:"original_template"` + Type string `json:"type"` + UpdatedAt string `json:"updated_at"` + LastEditedBy string `json:"last_edited_by,omitempty"` } // ContentResponse represents the API response structure diff --git a/lib/package-lock.json b/lib/package-lock.json index 01eb4fe..f9cb9bc 100644 --- a/lib/package-lock.json +++ b/lib/package-lock.json @@ -8,10 +8,6 @@ "name": "@insertr/lib", "version": "1.0.0", "license": "MIT", - "dependencies": { - "marked": "^16.2.1", - "turndown": "^7.2.1" - }, "devDependencies": { "@rollup/plugin-node-resolve": "^15.0.0", "@rollup/plugin-terser": "^0.4.0", @@ -68,12 +64,6 @@ "@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": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", @@ -264,18 +254,6 @@ "dev": true, "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": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -434,15 +412,6 @@ "engines": { "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" - } } } } diff --git a/lib/src/core/api-client.js b/lib/src/core/api-client.js index b41a530..9f2b625 100644 --- a/lib/src/core/api-client.js +++ b/lib/src/core/api-client.js @@ -33,7 +33,7 @@ export class ApiClient { try { const payload = { html_markup: htmlMarkup, // Always send HTML markup - server extracts ID or generates new one - value: content, + html_content: content, type: type, 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) { try { const response = await fetch(`${this.baseUrl}/${contentId}/versions?site_id=${this.siteId}`); diff --git a/lib/src/core/insertr.js b/lib/src/core/insertr.js index 067a69c..d1bcf82 100644 --- a/lib/src/core/insertr.js +++ b/lib/src/core/insertr.js @@ -134,24 +134,13 @@ export class InsertrCore { detectContentType(element) { const tag = element.tagName.toLowerCase(); - if (element.classList.contains('insertr-group')) { - return 'group'; + // Only return database-valid types: 'text' or 'link' + if (tag === 'a' || tag === 'button') { + return 'link'; } - switch (tag) { - case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': - 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'; - } + // All other elements are text content + return 'text'; } // Get all elements with their metadata, including group elements