Files
insertr/internal/content/database.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

236 lines
6.6 KiB
Go

package content
import (
"context"
"database/sql"
"fmt"
"time"
"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"
)
// 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
}
// NewDatabaseClient creates a new database content client
func NewDatabaseClient(database *db.Database) *DatabaseClient {
return &DatabaseClient{
db: database,
}
}
// GetContent fetches a single content item by ID
func (d *DatabaseClient) GetContent(siteID, contentID string) (*engine.ContentItem, error) {
ctx := context.Background()
var content interface{}
var err error
switch d.db.GetDBType() {
case "sqlite3":
content, err = d.db.GetSQLiteQueries().GetContent(ctx, sqlite.GetContentParams{
ID: contentID,
SiteID: siteID,
})
case "postgresql":
content, err = d.db.GetPostgreSQLQueries().GetContent(ctx, postgresql.GetContentParams{
ID: contentID,
SiteID: siteID,
})
default:
return nil, fmt.Errorf("unsupported database type: %s", d.db.GetDBType())
}
if err != nil {
if err == sql.ErrNoRows {
return nil, nil // Content not found, return nil without error
}
return nil, fmt.Errorf("database error: %w", err)
}
item := d.convertToContentItem(content)
return &item, nil
}
// GetBulkContent fetches multiple content items by IDs
func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) {
if len(contentIDs) == 0 {
return make(map[string]engine.ContentItem), nil
}
ctx := context.Background()
var dbContent interface{}
var err error
switch d.db.GetDBType() {
case "sqlite3":
dbContent, err = d.db.GetSQLiteQueries().GetBulkContent(ctx, sqlite.GetBulkContentParams{
SiteID: siteID,
Ids: contentIDs,
})
case "postgresql":
dbContent, err = d.db.GetPostgreSQLQueries().GetBulkContent(ctx, postgresql.GetBulkContentParams{
SiteID: siteID,
Ids: contentIDs,
})
default:
return nil, fmt.Errorf("unsupported database type: %s", d.db.GetDBType())
}
if err != nil {
return nil, fmt.Errorf("database error: %w", err)
}
items := d.convertToContentItemList(dbContent)
// Convert slice to map for easy lookup
result := make(map[string]engine.ContentItem)
for _, item := range items {
result[item.ID] = item
}
return result, nil
}
// GetAllContent fetches all content for a site
func (d *DatabaseClient) GetAllContent(siteID string) (map[string]engine.ContentItem, error) {
ctx := context.Background()
var dbContent interface{}
var err error
switch d.db.GetDBType() {
case "sqlite3":
dbContent, err = d.db.GetSQLiteQueries().GetAllContent(ctx, siteID)
case "postgresql":
dbContent, err = d.db.GetPostgreSQLQueries().GetAllContent(ctx, siteID)
default:
return nil, fmt.Errorf("unsupported database type: %s", d.db.GetDBType())
}
if err != nil {
return nil, fmt.Errorf("database error: %w", err)
}
items := d.convertToContentItemList(dbContent)
// Convert slice to map for easy lookup
result := make(map[string]engine.ContentItem)
for _, item := range items {
result[item.ID] = item
}
return result, nil
}
// 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 engine.ContentItem{
ID: c.ID,
SiteID: c.SiteID,
HTMLContent: c.HtmlContent,
OriginalTemplate: getStringFromNullString(c.OriginalTemplate),
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
}
case "postgresql":
c := content.(postgresql.Content)
return engine.ContentItem{
ID: c.ID,
SiteID: c.SiteID,
HTMLContent: c.HtmlContent,
OriginalTemplate: getStringFromNullString(c.OriginalTemplate),
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
}
}
return engine.ContentItem{} // Should never happen
}
// 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([]engine.ContentItem, len(list))
for i, content := range list {
items[i] = d.convertToContentItem(content)
}
return items
case "postgresql":
list := contentList.([]postgresql.Content)
items := make([]engine.ContentItem, len(list))
for i, content := range list {
items[i] = d.convertToContentItem(content)
}
return items
}
return []engine.ContentItem{} // Should never happen
}
// CreateContent creates a new content item
func (c *DatabaseClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, 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),
LastEditedBy: lastEditedBy,
})
if err != nil {
return nil, err
}
return &engine.ContentItem{
ID: content.ID,
SiteID: content.SiteID,
HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
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),
LastEditedBy: lastEditedBy,
})
if err != nil {
return nil, err
}
return &engine.ContentItem{
ID: content.ID,
SiteID: content.SiteID,
HTMLContent: content.HtmlContent,
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
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}
}