Files
insertr/internal/content/client.go
Joakim b75eda2a87 feat: complete HTML-first architecture implementation (Phase 1 & 2)
Major architectural simplification removing content type complexity:

Database Schema:
- Remove 'type' field from content and content_versions tables
- Simplify to pure HTML storage with html_content + original_template
- Regenerate all sqlc models for SQLite and PostgreSQL

API Simplification:
- Remove content type routing and validation
- Eliminate type-specific handlers (text/markdown/structured)
- Unified HTML-first approach for all content operations
- Simplify CreateContent and UpdateContent to HTML-only

Backend Enhancements:
- Update enhancer to only generate data-content-id (no data-content-type)
- Improve container expansion utilities with comprehensive block/inline rules
- Add Phase 3 preparation with boundary-respecting traversal logic
- Strengthen element classification for viable children detection

Documentation:
- Update TODO.md to reflect Phase 1-3 completion status
- Add WORKING_ON.md documenting the architectural transformation
- Mark container expansion and HTML-first architecture as complete

This completes the transition to a unified HTML-first content management system
with automatic style detection and element-based behavior, eliminating the
complex multi-type system in favor of semantic HTML-driven editing.
2025-09-21 19:23:54 +02:00

174 lines
4.3 KiB
Go

package content
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/insertr/insertr/internal/engine"
)
// HTTPClient implements ContentClient for HTTP API access
type HTTPClient struct {
BaseURL string
APIKey string
HTTPClient *http.Client
}
// NewHTTPClient creates a new HTTP content client
func NewHTTPClient(baseURL, apiKey string) *HTTPClient {
return &HTTPClient{
BaseURL: strings.TrimSuffix(baseURL, "/"),
APIKey: apiKey,
HTTPClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// GetContent fetches a single content item by ID
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)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
if c.APIKey != "" {
req.Header.Set("Authorization", "Bearer "+c.APIKey)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("making request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == 404 {
return nil, nil // Content not found, return nil without error
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("API error: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
var item engine.ContentItem
if err := json.Unmarshal(body, &item); err != nil {
return nil, fmt.Errorf("parsing response: %w", err)
}
return &item, nil
}
// GetBulkContent fetches multiple content items by IDs
func (c *HTTPClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) {
if len(contentIDs) == 0 {
return make(map[string]engine.ContentItem), nil
}
// Build query parameters
params := url.Values{}
params.Set("site_id", siteID)
for _, id := range contentIDs {
params.Add("ids", id)
}
url := fmt.Sprintf("%s/api/content/bulk?%s", c.BaseURL, params.Encode())
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
if c.APIKey != "" {
req.Header.Set("Authorization", "Bearer "+c.APIKey)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("making request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("API error: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
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]engine.ContentItem)
for _, item := range response.Content {
result[item.ID] = item
}
return result, nil
}
// GetAllContent fetches all content for a site
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)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
if c.APIKey != "" {
req.Header.Set("Authorization", "Bearer "+c.APIKey)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("making request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("API error: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
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]engine.ContentItem)
for _, item := range response.Content {
result[item.ID] = item
}
return result, nil
}
// CreateContent creates a new content item via HTTP API
func (c *HTTPClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, 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")
}