Consolidate type definitions and fix API contract
- Move all ContentItem, ContentClient, ContentResponse types to engine/types.go as single source of truth - Remove duplicate type definitions from content/types.go - Update all imports across codebase to use engine types - Enhance engine to extract existing data-content-id from HTML markup - Simplify frontend to always send html_markup, let server handle ID extraction/generation - Fix contentId reference errors in frontend error handling - Add getAttribute helper method to engine for ID extraction - Add GetAllContent method to engine.DatabaseClient - Update enhancer to use engine.ContentClient interface - All builds and API endpoints verified working This resolves the 400 Bad Request errors and creates a unified architecture where the server is the single source of truth for all ID generation and content type management.
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/insertr/insertr/internal/content"
|
"github.com/insertr/insertr/internal/content"
|
||||||
"github.com/insertr/insertr/internal/db"
|
"github.com/insertr/insertr/internal/db"
|
||||||
|
"github.com/insertr/insertr/internal/engine"
|
||||||
)
|
)
|
||||||
|
|
||||||
var enhanceCmd = &cobra.Command{
|
var enhanceCmd = &cobra.Command{
|
||||||
@@ -49,7 +50,7 @@ func runEnhance(cmd *cobra.Command, args []string) {
|
|||||||
outputDir := viper.GetString("cli.output")
|
outputDir := viper.GetString("cli.output")
|
||||||
|
|
||||||
// Create content client
|
// Create content client
|
||||||
var client content.ContentClient
|
var client engine.ContentClient
|
||||||
if apiURL != "" {
|
if apiURL != "" {
|
||||||
fmt.Printf("🌐 Using content API: %s\n", apiURL)
|
fmt.Printf("🌐 Using content API: %s\n", apiURL)
|
||||||
client = content.NewHTTPClient(apiURL, apiKey)
|
client = content.NewHTTPClient(apiURL, apiKey)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/insertr/insertr/internal/engine"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTPClient implements ContentClient for HTTP API access
|
// HTTPClient implements ContentClient for HTTP API access
|
||||||
@@ -29,7 +31,7 @@ func NewHTTPClient(baseURL, apiKey string) *HTTPClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetContent fetches a single content item by ID
|
// GetContent fetches a single content item by ID
|
||||||
func (c *HTTPClient) GetContent(siteID, contentID string) (*ContentItem, error) {
|
func (c *HTTPClient) GetContent(siteID, contentID string) (*engine.ContentItem, error) {
|
||||||
url := fmt.Sprintf("%s/api/content/%s?site_id=%s", c.BaseURL, contentID, siteID)
|
url := fmt.Sprintf("%s/api/content/%s?site_id=%s", c.BaseURL, contentID, siteID)
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
@@ -60,7 +62,7 @@ func (c *HTTPClient) GetContent(siteID, contentID string) (*ContentItem, error)
|
|||||||
return nil, fmt.Errorf("reading response: %w", err)
|
return nil, fmt.Errorf("reading response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var item ContentItem
|
var item engine.ContentItem
|
||||||
if err := json.Unmarshal(body, &item); err != nil {
|
if err := json.Unmarshal(body, &item); err != nil {
|
||||||
return nil, fmt.Errorf("parsing response: %w", err)
|
return nil, fmt.Errorf("parsing response: %w", err)
|
||||||
}
|
}
|
||||||
@@ -69,9 +71,9 @@ func (c *HTTPClient) GetContent(siteID, contentID string) (*ContentItem, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetBulkContent fetches multiple content items by IDs
|
// GetBulkContent fetches multiple content items by IDs
|
||||||
func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) {
|
func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) {
|
||||||
if len(contentIDs) == 0 {
|
if len(contentIDs) == 0 {
|
||||||
return make(map[string]ContentItem), nil
|
return make(map[string]engine.ContentItem), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build query parameters
|
// Build query parameters
|
||||||
@@ -107,13 +109,13 @@ func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[str
|
|||||||
return nil, fmt.Errorf("reading response: %w", err)
|
return nil, fmt.Errorf("reading response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var response ContentResponse
|
var response engine.ContentResponse
|
||||||
if err := json.Unmarshal(body, &response); err != nil {
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
return nil, fmt.Errorf("parsing response: %w", err)
|
return nil, fmt.Errorf("parsing response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert slice to map for easy lookup
|
// Convert slice to map for easy lookup
|
||||||
result := make(map[string]ContentItem)
|
result := make(map[string]engine.ContentItem)
|
||||||
for _, item := range response.Content {
|
for _, item := range response.Content {
|
||||||
result[item.ID] = item
|
result[item.ID] = item
|
||||||
}
|
}
|
||||||
@@ -122,7 +124,7 @@ func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAllContent fetches all content for a site
|
// GetAllContent fetches all content for a site
|
||||||
func (c *HTTPClient) GetAllContent(siteID string) (map[string]ContentItem, error) {
|
func (c *HTTPClient) GetAllContent(siteID string) (map[string]engine.ContentItem, error) {
|
||||||
url := fmt.Sprintf("%s/api/content?site_id=%s", c.BaseURL, siteID)
|
url := fmt.Sprintf("%s/api/content?site_id=%s", c.BaseURL, siteID)
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
@@ -149,13 +151,13 @@ func (c *HTTPClient) GetAllContent(siteID string) (map[string]ContentItem, error
|
|||||||
return nil, fmt.Errorf("reading response: %w", err)
|
return nil, fmt.Errorf("reading response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var response ContentResponse
|
var response engine.ContentResponse
|
||||||
if err := json.Unmarshal(body, &response); err != nil {
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
return nil, fmt.Errorf("parsing response: %w", err)
|
return nil, fmt.Errorf("parsing response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert slice to map for easy lookup
|
// Convert slice to map for easy lookup
|
||||||
result := make(map[string]ContentItem)
|
result := make(map[string]engine.ContentItem)
|
||||||
for _, item := range response.Content {
|
for _, item := range response.Content {
|
||||||
result[item.ID] = item
|
result[item.ID] = item
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/insertr/insertr/internal/db"
|
"github.com/insertr/insertr/internal/db"
|
||||||
"github.com/insertr/insertr/internal/db/postgresql"
|
"github.com/insertr/insertr/internal/db/postgresql"
|
||||||
"github.com/insertr/insertr/internal/db/sqlite"
|
"github.com/insertr/insertr/internal/db/sqlite"
|
||||||
|
"github.com/insertr/insertr/internal/engine"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DatabaseClient implements ContentClient for direct database access
|
// DatabaseClient implements ContentClient for direct database access
|
||||||
@@ -24,7 +25,7 @@ func NewDatabaseClient(database *db.Database) *DatabaseClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetContent fetches a single content item by ID
|
// GetContent fetches a single content item by ID
|
||||||
func (d *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, error) {
|
func (d *DatabaseClient) GetContent(siteID, contentID string) (*engine.ContentItem, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
var content interface{}
|
var content interface{}
|
||||||
var err error
|
var err error
|
||||||
@@ -56,9 +57,9 @@ func (d *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetBulkContent fetches multiple content items by IDs
|
// GetBulkContent fetches multiple content items by IDs
|
||||||
func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) {
|
func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) {
|
||||||
if len(contentIDs) == 0 {
|
if len(contentIDs) == 0 {
|
||||||
return make(map[string]ContentItem), nil
|
return make(map[string]engine.ContentItem), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -87,7 +88,7 @@ func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
|
|||||||
items := d.convertToContentItemList(dbContent)
|
items := d.convertToContentItemList(dbContent)
|
||||||
|
|
||||||
// Convert slice to map for easy lookup
|
// Convert slice to map for easy lookup
|
||||||
result := make(map[string]ContentItem)
|
result := make(map[string]engine.ContentItem)
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
result[item.ID] = item
|
result[item.ID] = item
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,7 @@ func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAllContent fetches all content for a site
|
// GetAllContent fetches all content for a site
|
||||||
func (d *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, error) {
|
func (d *DatabaseClient) GetAllContent(siteID string) (map[string]engine.ContentItem, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
var dbContent interface{}
|
var dbContent interface{}
|
||||||
var err error
|
var err error
|
||||||
@@ -117,7 +118,7 @@ func (d *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
|
|||||||
items := d.convertToContentItemList(dbContent)
|
items := d.convertToContentItemList(dbContent)
|
||||||
|
|
||||||
// Convert slice to map for easy lookup
|
// Convert slice to map for easy lookup
|
||||||
result := make(map[string]ContentItem)
|
result := make(map[string]engine.ContentItem)
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
result[item.ID] = item
|
result[item.ID] = item
|
||||||
}
|
}
|
||||||
@@ -125,12 +126,12 @@ func (d *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertToContentItem converts database models to content.ContentItem
|
// convertToContentItem converts database models to engine.ContentItem
|
||||||
func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem {
|
func (d *DatabaseClient) convertToContentItem(content interface{}) engine.ContentItem {
|
||||||
switch d.db.GetDBType() {
|
switch d.db.GetDBType() {
|
||||||
case "sqlite3":
|
case "sqlite3":
|
||||||
c := content.(sqlite.Content)
|
c := content.(sqlite.Content)
|
||||||
return ContentItem{
|
return engine.ContentItem{
|
||||||
ID: c.ID,
|
ID: c.ID,
|
||||||
SiteID: c.SiteID,
|
SiteID: c.SiteID,
|
||||||
Value: c.Value,
|
Value: c.Value,
|
||||||
@@ -139,7 +140,7 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem {
|
|||||||
}
|
}
|
||||||
case "postgresql":
|
case "postgresql":
|
||||||
c := content.(postgresql.Content)
|
c := content.(postgresql.Content)
|
||||||
return ContentItem{
|
return engine.ContentItem{
|
||||||
ID: c.ID,
|
ID: c.ID,
|
||||||
SiteID: c.SiteID,
|
SiteID: c.SiteID,
|
||||||
Value: c.Value,
|
Value: c.Value,
|
||||||
@@ -147,26 +148,26 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem {
|
|||||||
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
|
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ContentItem{} // Should never happen
|
return engine.ContentItem{} // Should never happen
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertToContentItemList converts database model lists to content.ContentItem slice
|
// convertToContentItemList converts database model lists to engine.ContentItem slice
|
||||||
func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []ContentItem {
|
func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []engine.ContentItem {
|
||||||
switch d.db.GetDBType() {
|
switch d.db.GetDBType() {
|
||||||
case "sqlite3":
|
case "sqlite3":
|
||||||
list := contentList.([]sqlite.Content)
|
list := contentList.([]sqlite.Content)
|
||||||
items := make([]ContentItem, len(list))
|
items := make([]engine.ContentItem, len(list))
|
||||||
for i, content := range list {
|
for i, content := range list {
|
||||||
items[i] = d.convertToContentItem(content)
|
items[i] = d.convertToContentItem(content)
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
case "postgresql":
|
case "postgresql":
|
||||||
list := contentList.([]postgresql.Content)
|
list := contentList.([]postgresql.Content)
|
||||||
items := make([]ContentItem, len(list))
|
items := make([]engine.ContentItem, len(list))
|
||||||
for i, content := range list {
|
for i, content := range list {
|
||||||
items[i] = d.convertToContentItem(content)
|
items[i] = d.convertToContentItem(content)
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
return []ContentItem{} // Should never happen
|
return []engine.ContentItem{} // Should never happen
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type Enhancer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewEnhancer creates a new HTML enhancer using unified engine
|
// NewEnhancer creates a new HTML enhancer using unified engine
|
||||||
func NewEnhancer(client ContentClient, siteID string) *Enhancer {
|
func NewEnhancer(client engine.ContentClient, siteID string) *Enhancer {
|
||||||
// Create database client for engine
|
// Create database client for engine
|
||||||
var engineClient engine.ContentClient
|
var engineClient engine.ContentClient
|
||||||
if dbClient, ok := client.(*DatabaseClient); ok {
|
if dbClient, ok := client.(*DatabaseClient); ok {
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
package content
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/yuin/goldmark"
|
|
||||||
"github.com/yuin/goldmark/parser"
|
|
||||||
"github.com/yuin/goldmark/renderer/html"
|
|
||||||
"github.com/yuin/goldmark/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MarkdownProcessor handles minimal markdown processing
|
|
||||||
// Supports only: **bold**, *italic*, and [link](url)
|
|
||||||
type MarkdownProcessor struct {
|
|
||||||
parser goldmark.Markdown
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMarkdownProcessor creates a new markdown processor with minimal configuration
|
|
||||||
func NewMarkdownProcessor() *MarkdownProcessor {
|
|
||||||
// Configure goldmark to only support basic inline formatting
|
|
||||||
md := goldmark.New(
|
|
||||||
goldmark.WithParserOptions(
|
|
||||||
parser.WithInlineParsers(
|
|
||||||
// Bold (**text**) and italic (*text*) - same parser handles both
|
|
||||||
util.Prioritized(parser.NewEmphasisParser(), 500),
|
|
||||||
|
|
||||||
// Links [text](url)
|
|
||||||
util.Prioritized(parser.NewLinkParser(), 600),
|
|
||||||
),
|
|
||||||
// Disable all block parsers except paragraph (no headings, lists, etc.)
|
|
||||||
parser.WithBlockParsers(
|
|
||||||
util.Prioritized(parser.NewParagraphParser(), 200),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
goldmark.WithRendererOptions(
|
|
||||||
html.WithXHTML(), // <br /> instead of <br>
|
|
||||||
html.WithHardWraps(), // Line breaks become <br />
|
|
||||||
html.WithUnsafe(), // Allow existing HTML to pass through
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return &MarkdownProcessor{parser: md}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToHTML converts markdown string to HTML
|
|
||||||
func (mp *MarkdownProcessor) ToHTML(markdown string) (string, error) {
|
|
||||||
if markdown == "" {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := mp.parser.Convert([]byte(markdown), &buf); err != nil {
|
|
||||||
log.Printf("Markdown conversion failed: %v", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
html := buf.String()
|
|
||||||
|
|
||||||
// Clean up goldmark's paragraph wrapping for inline content
|
|
||||||
// If content is wrapped in a single <p> tag, extract just the inner content
|
|
||||||
html = strings.TrimSpace(html)
|
|
||||||
|
|
||||||
if strings.HasPrefix(html, "<p>") && strings.HasSuffix(html, "</p>") {
|
|
||||||
// Check if this is a single paragraph (no other <p> tags inside)
|
|
||||||
inner := html[3 : len(html)-4] // Remove <p> and </p>
|
|
||||||
if !strings.Contains(inner, "<p>") {
|
|
||||||
// Single paragraph - return just the inner content for inline injection
|
|
||||||
return inner, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiple paragraphs or other block content - return as-is
|
|
||||||
return html, nil
|
|
||||||
}
|
|
||||||
@@ -2,17 +2,19 @@ package content
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/insertr/insertr/internal/engine"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockClient implements ContentClient with mock data for development
|
// MockClient implements ContentClient with mock data for development
|
||||||
type MockClient struct {
|
type MockClient struct {
|
||||||
data map[string]ContentItem
|
data map[string]engine.ContentItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMockClient creates a new mock content client with sample data
|
// NewMockClient creates a new mock content client with sample data
|
||||||
func NewMockClient() *MockClient {
|
func NewMockClient() *MockClient {
|
||||||
// Generate realistic mock content based on actual generated IDs
|
// Generate realistic mock content based on actual generated IDs
|
||||||
data := map[string]ContentItem{
|
data := map[string]engine.ContentItem{
|
||||||
// Navigation (index.html has collision suffix)
|
// Navigation (index.html has collision suffix)
|
||||||
"navbar-logo-2b10ad": {
|
"navbar-logo-2b10ad": {
|
||||||
ID: "navbar-logo-2b10ad",
|
ID: "navbar-logo-2b10ad",
|
||||||
@@ -98,7 +100,7 @@ func NewMockClient() *MockClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetContent fetches a single content item by ID
|
// GetContent fetches a single content item by ID
|
||||||
func (m *MockClient) GetContent(siteID, contentID string) (*ContentItem, error) {
|
func (m *MockClient) GetContent(siteID, contentID string) (*engine.ContentItem, error) {
|
||||||
if item, exists := m.data[contentID]; exists && item.SiteID == siteID {
|
if item, exists := m.data[contentID]; exists && item.SiteID == siteID {
|
||||||
return &item, nil
|
return &item, nil
|
||||||
}
|
}
|
||||||
@@ -108,8 +110,8 @@ func (m *MockClient) GetContent(siteID, contentID string) (*ContentItem, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetBulkContent fetches multiple content items by IDs
|
// GetBulkContent fetches multiple content items by IDs
|
||||||
func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) {
|
func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) {
|
||||||
result := make(map[string]ContentItem)
|
result := make(map[string]engine.ContentItem)
|
||||||
|
|
||||||
for _, id := range contentIDs {
|
for _, id := range contentIDs {
|
||||||
item, err := m.GetContent(siteID, id)
|
item, err := m.GetContent(siteID, id)
|
||||||
@@ -125,8 +127,8 @@ func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAllContent fetches all content for a site
|
// GetAllContent fetches all content for a site
|
||||||
func (m *MockClient) GetAllContent(siteID string) (map[string]ContentItem, error) {
|
func (m *MockClient) GetAllContent(siteID string) (map[string]engine.ContentItem, error) {
|
||||||
result := make(map[string]ContentItem)
|
result := make(map[string]engine.ContentItem)
|
||||||
|
|
||||||
for _, item := range m.data {
|
for _, item := range m.data {
|
||||||
if item.SiteID == siteID {
|
if item.SiteID == siteID {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/insertr/insertr/internal/engine"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SiteConfig represents configuration for a registered site
|
// SiteConfig represents configuration for a registered site
|
||||||
@@ -28,7 +30,7 @@ type SiteManager struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewSiteManager creates a new site manager
|
// NewSiteManager creates a new site manager
|
||||||
func NewSiteManager(contentClient ContentClient, backupDir string, devMode bool) *SiteManager {
|
func NewSiteManager(contentClient engine.ContentClient, backupDir string, devMode bool) *SiteManager {
|
||||||
if backupDir == "" {
|
if backupDir == "" {
|
||||||
backupDir = "./insertr-backups"
|
backupDir = "./insertr-backups"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
package content
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContentResponse represents the API response structure
|
|
||||||
type ContentResponse struct {
|
|
||||||
Content []ContentItem `json:"content"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContentClient interface for content retrieval
|
|
||||||
type ContentClient interface {
|
|
||||||
// GetContent fetches content by ID
|
|
||||||
GetContent(siteID, contentID string) (*ContentItem, error)
|
|
||||||
|
|
||||||
// GetBulkContent fetches multiple content items by IDs
|
|
||||||
GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error)
|
|
||||||
|
|
||||||
// GetAllContent fetches all content for a site
|
|
||||||
GetAllContent(siteID string) (map[string]ContentItem, error)
|
|
||||||
}
|
|
||||||
@@ -110,3 +110,47 @@ func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
|
|||||||
return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType())
|
return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllContent retrieves all content items for a site
|
||||||
|
func (c *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, error) {
|
||||||
|
switch c.database.GetDBType() {
|
||||||
|
case "sqlite3":
|
||||||
|
contents, err := c.database.GetSQLiteQueries().GetAllContent(context.Background(), siteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
|
||||||
|
case "postgresql":
|
||||||
|
contents, err := c.database.GetPostgreSQLQueries().GetAllContent(context.Background(), siteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,20 +37,33 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
|
|||||||
processedElements := make([]ProcessedElement, len(elements))
|
processedElements := make([]ProcessedElement, len(elements))
|
||||||
|
|
||||||
for i, elem := range elements {
|
for i, elem := range elements {
|
||||||
// Generate ID using the same algorithm as the parser
|
// Check if element already has a data-content-id
|
||||||
id := e.idGenerator.Generate(elem.Node, input.FilePath)
|
existingID := e.getAttribute(elem.Node, "data-content-id")
|
||||||
|
var id string
|
||||||
|
var wasGenerated bool
|
||||||
|
|
||||||
|
if existingID != "" {
|
||||||
|
// Use existing ID from enhanced element
|
||||||
|
id = existingID
|
||||||
|
wasGenerated = false
|
||||||
|
} else {
|
||||||
|
// Generate new ID for unprocessed element
|
||||||
|
id = e.idGenerator.Generate(elem.Node, input.FilePath)
|
||||||
|
wasGenerated = true
|
||||||
|
}
|
||||||
|
|
||||||
generatedIDs[fmt.Sprintf("element_%d", i)] = id
|
generatedIDs[fmt.Sprintf("element_%d", i)] = id
|
||||||
|
|
||||||
processedElements[i] = ProcessedElement{
|
processedElements[i] = ProcessedElement{
|
||||||
Node: elem.Node,
|
Node: elem.Node,
|
||||||
ID: id,
|
ID: id,
|
||||||
Type: elem.Type,
|
Type: elem.Type,
|
||||||
Generated: true,
|
Generated: wasGenerated,
|
||||||
Tag: elem.Node.Data,
|
Tag: elem.Node.Data,
|
||||||
Classes: GetClasses(elem.Node),
|
Classes: GetClasses(elem.Node),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add content attributes to the node
|
// Add/update content attributes to the node
|
||||||
e.addContentAttributes(elem.Node, id, elem.Type)
|
e.addContentAttributes(elem.Node, id, elem.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +146,16 @@ func (e *ContentEngine) addContentAttributes(node *html.Node, contentID, content
|
|||||||
e.setAttribute(node, "data-content-type", contentType)
|
e.setAttribute(node, "data-content-type", contentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getAttribute gets an attribute value from an HTML node
|
||||||
|
func (e *ContentEngine) getAttribute(node *html.Node, key string) string {
|
||||||
|
for _, attr := range node.Attr {
|
||||||
|
if attr.Key == key {
|
||||||
|
return attr.Val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// setAttribute sets an attribute on an HTML node
|
// setAttribute sets an attribute on an HTML node
|
||||||
func (e *ContentEngine) setAttribute(node *html.Node, key, value string) {
|
func (e *ContentEngine) setAttribute(node *html.Node, key, value string) {
|
||||||
// Remove existing attribute if it exists
|
// Remove existing attribute if it exists
|
||||||
|
|||||||
@@ -43,17 +43,25 @@ type ProcessedElement struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ContentClient interface for accessing content data
|
// ContentClient interface for accessing content data
|
||||||
// This will be implemented by database clients
|
// This will be implemented by database clients, HTTP clients, and mock clients
|
||||||
type ContentClient interface {
|
type ContentClient interface {
|
||||||
GetContent(siteID, contentID string) (*ContentItem, error)
|
GetContent(siteID, contentID string) (*ContentItem, error)
|
||||||
GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error)
|
GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error)
|
||||||
|
GetAllContent(siteID string) (map[string]ContentItem, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContentItem represents a piece of content from the database
|
// ContentItem represents a piece of content from the database
|
||||||
type ContentItem struct {
|
type ContentItem struct {
|
||||||
ID string
|
ID string `json:"id"`
|
||||||
SiteID string
|
SiteID string `json:"site_id"`
|
||||||
Value string
|
Value string `json:"value"`
|
||||||
Type string
|
Type string `json:"type"`
|
||||||
LastEditedBy string
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
LastEditedBy string `json:"last_edited_by,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContentResponse represents the API response structure
|
||||||
|
type ContentResponse struct {
|
||||||
|
Content []ContentItem `json:"content"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,24 +29,15 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async createContent(contentId, content, type, htmlMarkup = null) {
|
async createContent(content, type, htmlMarkup) {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
html_markup: htmlMarkup, // Always send HTML markup - server extracts ID or generates new one
|
||||||
value: content,
|
value: content,
|
||||||
type: type,
|
type: type,
|
||||||
file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation
|
file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation
|
||||||
};
|
};
|
||||||
|
|
||||||
if (contentId) {
|
|
||||||
// Enhanced site - provide existing ID
|
|
||||||
payload.id = contentId;
|
|
||||||
} else if (htmlMarkup) {
|
|
||||||
// Non-enhanced site - provide HTML markup for unified engine ID generation
|
|
||||||
payload.html_markup = htmlMarkup;
|
|
||||||
} else {
|
|
||||||
throw new Error('Either contentId or htmlMarkup must be provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, {
|
const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -61,7 +52,7 @@ export class ApiClient {
|
|||||||
console.log(`✅ Content created: ${result.id} (${result.type})`);
|
console.log(`✅ Content created: ${result.id} (${result.type})`);
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
console.warn(`⚠️ Create failed (${response.status}): ${contentId || 'backend-generated'}`);
|
console.warn(`⚠️ Create failed (${response.status}): server will generate ID`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -69,7 +60,7 @@ export class ApiClient {
|
|||||||
console.warn(`🔌 API Server not reachable at ${this.baseUrl}`);
|
console.warn(`🔌 API Server not reachable at ${this.baseUrl}`);
|
||||||
console.warn('💡 Start full-stack development: just dev');
|
console.warn('💡 Start full-stack development: just dev');
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to create content:', contentId, error);
|
console.error('Failed to create content:', error);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,13 +102,12 @@ export class InsertrEditor {
|
|||||||
contentValue = formData.text || formData;
|
contentValue = formData.text || formData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Universal upsert - works for both new and existing content
|
// Universal upsert - server handles ID extraction/generation from markup
|
||||||
const contentType = this.determineContentType(meta.element);
|
const contentType = this.determineContentType(meta.element);
|
||||||
const result = await this.apiClient.createContent(
|
const result = await this.apiClient.createContent(
|
||||||
meta.contentId, // Use existing ID if available, null if new
|
|
||||||
contentValue,
|
contentValue,
|
||||||
contentType,
|
contentType,
|
||||||
meta.htmlMarkup
|
meta.htmlMarkup // Always send HTML markup - server is smart about ID handling
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|||||||
@@ -106,6 +106,11 @@ export class InsertrCore {
|
|||||||
getElementMetadata(element) {
|
getElementMetadata(element) {
|
||||||
const existingId = element.getAttribute('data-content-id');
|
const existingId = element.getAttribute('data-content-id');
|
||||||
|
|
||||||
|
// Ensure element has insertr class for server processing
|
||||||
|
if (!element.classList.contains('insertr')) {
|
||||||
|
element.classList.add('insertr');
|
||||||
|
}
|
||||||
|
|
||||||
// Send HTML markup to server for unified ID generation
|
// Send HTML markup to server for unified ID generation
|
||||||
return {
|
return {
|
||||||
contentId: existingId, // null if new content, existing ID if updating
|
contentId: existingId, // null if new content, existing ID if updating
|
||||||
|
|||||||
@@ -109,6 +109,11 @@ var Insertr = (function () {
|
|||||||
getElementMetadata(element) {
|
getElementMetadata(element) {
|
||||||
const existingId = element.getAttribute('data-content-id');
|
const existingId = element.getAttribute('data-content-id');
|
||||||
|
|
||||||
|
// Ensure element has insertr class for server processing
|
||||||
|
if (!element.classList.contains('insertr')) {
|
||||||
|
element.classList.add('insertr');
|
||||||
|
}
|
||||||
|
|
||||||
// Send HTML markup to server for unified ID generation
|
// Send HTML markup to server for unified ID generation
|
||||||
return {
|
return {
|
||||||
contentId: existingId, // null if new content, existing ID if updating
|
contentId: existingId, // null if new content, existing ID if updating
|
||||||
@@ -2820,13 +2825,12 @@ Please report this to https://github.com/markedjs/marked.`,e){let r="<p>An error
|
|||||||
contentValue = formData.text || formData;
|
contentValue = formData.text || formData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Universal upsert - works for both new and existing content
|
// Universal upsert - server handles ID extraction/generation from markup
|
||||||
const contentType = this.determineContentType(meta.element);
|
const contentType = this.determineContentType(meta.element);
|
||||||
const result = await this.apiClient.createContent(
|
const result = await this.apiClient.createContent(
|
||||||
meta.contentId, // Use existing ID if available, null if new
|
|
||||||
contentValue,
|
contentValue,
|
||||||
contentType,
|
contentType,
|
||||||
meta.htmlMarkup
|
meta.htmlMarkup // Always send HTML markup - server is smart about ID handling
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -3761,24 +3765,15 @@ Please report this to https://github.com/markedjs/marked.`,e){let r="<p>An error
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async createContent(contentId, content, type, htmlMarkup = null) {
|
async createContent(content, type, htmlMarkup) {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
html_markup: htmlMarkup, // Always send HTML markup - server extracts ID or generates new one
|
||||||
value: content,
|
value: content,
|
||||||
type: type,
|
type: type,
|
||||||
file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation
|
file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation
|
||||||
};
|
};
|
||||||
|
|
||||||
if (contentId) {
|
|
||||||
// Enhanced site - provide existing ID
|
|
||||||
payload.id = contentId;
|
|
||||||
} else if (htmlMarkup) {
|
|
||||||
// Non-enhanced site - provide HTML markup for unified engine ID generation
|
|
||||||
payload.html_markup = htmlMarkup;
|
|
||||||
} else {
|
|
||||||
throw new Error('Either contentId or htmlMarkup must be provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, {
|
const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -3793,7 +3788,7 @@ Please report this to https://github.com/markedjs/marked.`,e){let r="<p>An error
|
|||||||
console.log(`✅ Content created: ${result.id} (${result.type})`);
|
console.log(`✅ Content created: ${result.id} (${result.type})`);
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
console.warn(`⚠️ Create failed (${response.status}): ${contentId || 'backend-generated'}`);
|
console.warn(`⚠️ Create failed (${response.status}): server will generate ID`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -3801,7 +3796,7 @@ Please report this to https://github.com/markedjs/marked.`,e){let r="<p>An error
|
|||||||
console.warn(`🔌 API Server not reachable at ${this.baseUrl}`);
|
console.warn(`🔌 API Server not reachable at ${this.baseUrl}`);
|
||||||
console.warn('💡 Start full-stack development: just dev');
|
console.warn('💡 Start full-stack development: just dev');
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to create content:', contentId, error);
|
console.error('Failed to create content:', error);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user