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:
@@ -8,6 +8,8 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/insertr/insertr/internal/engine"
|
||||
)
|
||||
|
||||
// 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
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
var item ContentItem
|
||||
var item engine.ContentItem
|
||||
if err := json.Unmarshal(body, &item); err != nil {
|
||||
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
|
||||
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 {
|
||||
return make(map[string]ContentItem), nil
|
||||
return make(map[string]engine.ContentItem), nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
var response ContentResponse
|
||||
var response engine.ContentResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, fmt.Errorf("parsing response: %w", err)
|
||||
}
|
||||
|
||||
// Convert slice to map for easy lookup
|
||||
result := make(map[string]ContentItem)
|
||||
result := make(map[string]engine.ContentItem)
|
||||
for _, item := range response.Content {
|
||||
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
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
var response ContentResponse
|
||||
var response engine.ContentResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, fmt.Errorf("parsing response: %w", err)
|
||||
}
|
||||
|
||||
// Convert slice to map for easy lookup
|
||||
result := make(map[string]ContentItem)
|
||||
result := make(map[string]engine.ContentItem)
|
||||
for _, item := range response.Content {
|
||||
result[item.ID] = item
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/insertr/insertr/internal/db"
|
||||
"github.com/insertr/insertr/internal/db/postgresql"
|
||||
"github.com/insertr/insertr/internal/db/sqlite"
|
||||
"github.com/insertr/insertr/internal/engine"
|
||||
)
|
||||
|
||||
// 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
|
||||
func (d *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, error) {
|
||||
func (d *DatabaseClient) GetContent(siteID, contentID string) (*engine.ContentItem, error) {
|
||||
ctx := context.Background()
|
||||
var content interface{}
|
||||
var err error
|
||||
@@ -56,9 +57,9 @@ func (d *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return make(map[string]ContentItem), nil
|
||||
return make(map[string]engine.ContentItem), nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -87,7 +88,7 @@ func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
|
||||
items := d.convertToContentItemList(dbContent)
|
||||
|
||||
// Convert slice to map for easy lookup
|
||||
result := make(map[string]ContentItem)
|
||||
result := make(map[string]engine.ContentItem)
|
||||
for _, item := range items {
|
||||
result[item.ID] = item
|
||||
}
|
||||
@@ -96,7 +97,7 @@ func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map
|
||||
}
|
||||
|
||||
// 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()
|
||||
var dbContent interface{}
|
||||
var err error
|
||||
@@ -117,7 +118,7 @@ func (d *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
|
||||
items := d.convertToContentItemList(dbContent)
|
||||
|
||||
// Convert slice to map for easy lookup
|
||||
result := make(map[string]ContentItem)
|
||||
result := make(map[string]engine.ContentItem)
|
||||
for _, item := range items {
|
||||
result[item.ID] = item
|
||||
}
|
||||
@@ -125,12 +126,12 @@ func (d *DatabaseClient) GetAllContent(siteID string) (map[string]ContentItem, e
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// convertToContentItem converts database models to content.ContentItem
|
||||
func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem {
|
||||
// convertToContentItem converts database models to engine.ContentItem
|
||||
func (d *DatabaseClient) convertToContentItem(content interface{}) engine.ContentItem {
|
||||
switch d.db.GetDBType() {
|
||||
case "sqlite3":
|
||||
c := content.(sqlite.Content)
|
||||
return ContentItem{
|
||||
return engine.ContentItem{
|
||||
ID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
Value: c.Value,
|
||||
@@ -139,7 +140,7 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem {
|
||||
}
|
||||
case "postgresql":
|
||||
c := content.(postgresql.Content)
|
||||
return ContentItem{
|
||||
return engine.ContentItem{
|
||||
ID: c.ID,
|
||||
SiteID: c.SiteID,
|
||||
Value: c.Value,
|
||||
@@ -147,26 +148,26 @@ func (d *DatabaseClient) convertToContentItem(content interface{}) ContentItem {
|
||||
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
|
||||
func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []ContentItem {
|
||||
// convertToContentItemList converts database model lists to engine.ContentItem slice
|
||||
func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []engine.ContentItem {
|
||||
switch d.db.GetDBType() {
|
||||
case "sqlite3":
|
||||
list := contentList.([]sqlite.Content)
|
||||
items := make([]ContentItem, len(list))
|
||||
items := make([]engine.ContentItem, len(list))
|
||||
for i, content := range list {
|
||||
items[i] = d.convertToContentItem(content)
|
||||
}
|
||||
return items
|
||||
case "postgresql":
|
||||
list := contentList.([]postgresql.Content)
|
||||
items := make([]ContentItem, len(list))
|
||||
items := make([]engine.ContentItem, len(list))
|
||||
for i, content := range list {
|
||||
items[i] = d.convertToContentItem(content)
|
||||
}
|
||||
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
|
||||
func NewEnhancer(client ContentClient, siteID string) *Enhancer {
|
||||
func NewEnhancer(client engine.ContentClient, siteID string) *Enhancer {
|
||||
// Create database client for engine
|
||||
var engineClient engine.ContentClient
|
||||
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 (
|
||||
"time"
|
||||
|
||||
"github.com/insertr/insertr/internal/engine"
|
||||
)
|
||||
|
||||
// MockClient implements ContentClient with mock data for development
|
||||
type MockClient struct {
|
||||
data map[string]ContentItem
|
||||
data map[string]engine.ContentItem
|
||||
}
|
||||
|
||||
// NewMockClient creates a new mock content client with sample data
|
||||
func NewMockClient() *MockClient {
|
||||
// Generate realistic mock content based on actual generated IDs
|
||||
data := map[string]ContentItem{
|
||||
data := map[string]engine.ContentItem{
|
||||
// Navigation (index.html has collision suffix)
|
||||
"navbar-logo-2b10ad": {
|
||||
ID: "navbar-logo-2b10ad",
|
||||
@@ -98,7 +100,7 @@ func NewMockClient() *MockClient {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return &item, nil
|
||||
}
|
||||
@@ -108,8 +110,8 @@ func (m *MockClient) GetContent(siteID, contentID string) (*ContentItem, error)
|
||||
}
|
||||
|
||||
// GetBulkContent fetches multiple content items by IDs
|
||||
func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) {
|
||||
result := make(map[string]ContentItem)
|
||||
func (m *MockClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) {
|
||||
result := make(map[string]engine.ContentItem)
|
||||
|
||||
for _, id := range contentIDs {
|
||||
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
|
||||
func (m *MockClient) GetAllContent(siteID string) (map[string]ContentItem, error) {
|
||||
result := make(map[string]ContentItem)
|
||||
func (m *MockClient) GetAllContent(siteID string) (map[string]engine.ContentItem, error) {
|
||||
result := make(map[string]engine.ContentItem)
|
||||
|
||||
for _, item := range m.data {
|
||||
if item.SiteID == siteID {
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/insertr/insertr/internal/engine"
|
||||
)
|
||||
|
||||
// SiteConfig represents configuration for a registered site
|
||||
@@ -28,7 +30,7 @@ type SiteManager struct {
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user