- Add class-based template comparison to differentiate styling variants - Implement template deduplication based on structure + class signatures - Add GetCollectionTemplate method to repository interface and implementations - Fix collection item creation by replacing unimplemented CreateCollectionItemAtomic - Add template selection modal with auto-default selection in frontend - Generate meaningful template names from distinctive CSS classes - Fix unique constraint violations with timestamp-based collection item IDs - Add collection templates API endpoint for frontend template fetching - Update simple demo with featured/compact/dark testimonial variants for testing
352 lines
11 KiB
Go
352 lines
11 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
"github.com/insertr/insertr/internal/db/postgresql"
|
|
)
|
|
|
|
// PostgreSQLRepository implements ContentRepository for PostgreSQL databases
|
|
type PostgreSQLRepository struct {
|
|
queries *postgresql.Queries
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewPostgreSQLRepository creates a new PostgreSQL repository
|
|
func NewPostgreSQLRepository(db *sql.DB) *PostgreSQLRepository {
|
|
return &PostgreSQLRepository{
|
|
queries: postgresql.New(db),
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// GetContent retrieves a single content item
|
|
func (r *PostgreSQLRepository) GetContent(ctx context.Context, siteID, contentID string) (*ContentItem, error) {
|
|
content, err := r.queries.GetContent(ctx, postgresql.GetContentParams{
|
|
ID: contentID,
|
|
SiteID: siteID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ContentItem{
|
|
ID: content.ID,
|
|
SiteID: content.SiteID,
|
|
HTMLContent: content.HtmlContent,
|
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
|
LastEditedBy: content.LastEditedBy,
|
|
}, nil
|
|
}
|
|
|
|
// GetBulkContent retrieves multiple content items
|
|
func (r *PostgreSQLRepository) GetBulkContent(ctx context.Context, siteID string, contentIDs []string) (map[string]ContentItem, error) {
|
|
contents, err := r.queries.GetBulkContent(ctx, postgresql.GetBulkContentParams{
|
|
SiteID: siteID,
|
|
Ids: contentIDs,
|
|
})
|
|
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,
|
|
HTMLContent: content.HtmlContent,
|
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
|
LastEditedBy: content.LastEditedBy,
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// GetAllContent retrieves all content items for a site
|
|
func (r *PostgreSQLRepository) GetAllContent(ctx context.Context, siteID string) (map[string]ContentItem, error) {
|
|
contents, err := r.queries.GetAllContent(ctx, 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,
|
|
HTMLContent: content.HtmlContent,
|
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
|
LastEditedBy: content.LastEditedBy,
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// CreateContent creates a new content item
|
|
func (r *PostgreSQLRepository) CreateContent(ctx context.Context, siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*ContentItem, error) {
|
|
content, err := r.queries.CreateContent(ctx, postgresql.CreateContentParams{
|
|
ID: contentID,
|
|
SiteID: siteID,
|
|
HtmlContent: htmlContent,
|
|
OriginalTemplate: ToNullString(originalTemplate),
|
|
LastEditedBy: lastEditedBy,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ContentItem{
|
|
ID: content.ID,
|
|
SiteID: content.SiteID,
|
|
HTMLContent: content.HtmlContent,
|
|
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
|
LastEditedBy: content.LastEditedBy,
|
|
}, nil
|
|
}
|
|
|
|
// GetCollection retrieves a collection container
|
|
func (r *PostgreSQLRepository) GetCollection(ctx context.Context, siteID, collectionID string) (*CollectionItem, error) {
|
|
collection, err := r.queries.GetCollection(ctx, postgresql.GetCollectionParams{
|
|
ID: collectionID,
|
|
SiteID: siteID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &CollectionItem{
|
|
ID: collection.ID,
|
|
SiteID: collection.SiteID,
|
|
ContainerHTML: collection.ContainerHtml,
|
|
LastEditedBy: collection.LastEditedBy,
|
|
}, nil
|
|
}
|
|
|
|
// CreateCollection creates a new collection container
|
|
func (r *PostgreSQLRepository) CreateCollection(ctx context.Context, siteID, collectionID, containerHTML, lastEditedBy string) (*CollectionItem, error) {
|
|
collection, err := r.queries.CreateCollection(ctx, postgresql.CreateCollectionParams{
|
|
ID: collectionID,
|
|
SiteID: siteID,
|
|
ContainerHtml: containerHTML,
|
|
LastEditedBy: lastEditedBy,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &CollectionItem{
|
|
ID: collection.ID,
|
|
SiteID: collection.SiteID,
|
|
ContainerHTML: collection.ContainerHtml,
|
|
LastEditedBy: collection.LastEditedBy,
|
|
}, nil
|
|
}
|
|
|
|
// GetCollectionItems retrieves all items in a collection with template information
|
|
func (r *PostgreSQLRepository) GetCollectionItems(ctx context.Context, siteID, collectionID string) ([]CollectionItemWithTemplate, error) {
|
|
items, err := r.queries.GetCollectionItemsWithTemplate(ctx, postgresql.GetCollectionItemsWithTemplateParams{
|
|
CollectionID: collectionID,
|
|
SiteID: siteID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]CollectionItemWithTemplate, len(items))
|
|
for i, item := range items {
|
|
result[i] = CollectionItemWithTemplate{
|
|
ItemID: item.ItemID,
|
|
CollectionID: item.CollectionID,
|
|
SiteID: item.SiteID,
|
|
TemplateID: int(item.TemplateID),
|
|
HTMLContent: item.HtmlContent,
|
|
Position: int(item.Position),
|
|
LastEditedBy: item.LastEditedBy,
|
|
TemplateName: item.TemplateName,
|
|
HTMLTemplate: item.HtmlTemplate,
|
|
IsDefault: item.IsDefault, // PostgreSQL uses BOOLEAN
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// CreateCollectionTemplate creates a new template for a collection
|
|
func (r *PostgreSQLRepository) CreateCollectionTemplate(ctx context.Context, siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error) {
|
|
template, err := r.queries.CreateCollectionTemplate(ctx, postgresql.CreateCollectionTemplateParams{
|
|
CollectionID: collectionID,
|
|
SiteID: siteID,
|
|
Name: name,
|
|
HtmlTemplate: htmlTemplate,
|
|
IsDefault: isDefault,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &CollectionTemplateItem{
|
|
TemplateID: int(template.TemplateID),
|
|
CollectionID: template.CollectionID,
|
|
SiteID: template.SiteID,
|
|
Name: template.Name,
|
|
HTMLTemplate: template.HtmlTemplate,
|
|
IsDefault: template.IsDefault,
|
|
}, nil
|
|
}
|
|
|
|
// GetCollectionTemplates retrieves all templates for a collection
|
|
func (r *PostgreSQLRepository) GetCollectionTemplates(ctx context.Context, siteID, collectionID string) ([]CollectionTemplateItem, error) {
|
|
templates, err := r.queries.GetCollectionTemplates(ctx, postgresql.GetCollectionTemplatesParams{
|
|
CollectionID: collectionID,
|
|
SiteID: siteID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]CollectionTemplateItem, len(templates))
|
|
for i, template := range templates {
|
|
result[i] = CollectionTemplateItem{
|
|
TemplateID: int(template.TemplateID),
|
|
CollectionID: template.CollectionID,
|
|
SiteID: template.SiteID,
|
|
Name: template.Name,
|
|
HTMLTemplate: template.HtmlTemplate,
|
|
IsDefault: template.IsDefault, // PostgreSQL uses BOOLEAN
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// GetCollectionTemplate retrieves a single template by ID
|
|
func (r *PostgreSQLRepository) GetCollectionTemplate(ctx context.Context, templateID int) (*CollectionTemplateItem, error) {
|
|
template, err := r.queries.GetCollectionTemplate(ctx, int32(templateID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := &CollectionTemplateItem{
|
|
TemplateID: int(template.TemplateID),
|
|
CollectionID: template.CollectionID,
|
|
SiteID: template.SiteID,
|
|
Name: template.Name,
|
|
HTMLTemplate: template.HtmlTemplate,
|
|
IsDefault: template.IsDefault, // PostgreSQL uses BOOLEAN
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// CreateCollectionItem creates a new collection item
|
|
func (r *PostgreSQLRepository) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) {
|
|
item, err := r.queries.CreateCollectionItem(ctx, postgresql.CreateCollectionItemParams{
|
|
ItemID: itemID,
|
|
CollectionID: collectionID,
|
|
SiteID: siteID,
|
|
TemplateID: int32(templateID),
|
|
HtmlContent: htmlContent,
|
|
Position: int32(position),
|
|
LastEditedBy: lastEditedBy,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &CollectionItemWithTemplate{
|
|
ItemID: item.ItemID,
|
|
CollectionID: item.CollectionID,
|
|
SiteID: item.SiteID,
|
|
TemplateID: int(item.TemplateID),
|
|
HTMLContent: item.HtmlContent,
|
|
Position: int(item.Position),
|
|
LastEditedBy: item.LastEditedBy,
|
|
}, nil
|
|
}
|
|
|
|
// CreateCollectionItemAtomic creates a collection item with all its content entries atomically
|
|
func (r *PostgreSQLRepository) CreateCollectionItemAtomic(ctx context.Context, siteID, collectionID string, templateID int, lastEditedBy string) (*CollectionItemWithTemplate, error) {
|
|
// Get template HTML for processing
|
|
templates, err := r.GetCollectionTemplates(ctx, siteID, collectionID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get templates: %w", err)
|
|
}
|
|
|
|
var templateHTML string
|
|
for _, template := range templates {
|
|
if template.TemplateID == templateID {
|
|
templateHTML = template.HTMLTemplate
|
|
break
|
|
}
|
|
}
|
|
|
|
if templateHTML == "" {
|
|
return nil, fmt.Errorf("template %d not found", templateID)
|
|
}
|
|
|
|
// TODO: Implement using unified engine approach
|
|
// This requires circular dependency resolution
|
|
return nil, fmt.Errorf("CreateCollectionItemAtomic not yet implemented for PostgreSQL")
|
|
}
|
|
|
|
// UpdateContent updates an existing content item
|
|
func (r *PostgreSQLRepository) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) {
|
|
content, err := r.queries.UpdateContent(ctx, postgresql.UpdateContentParams{
|
|
HtmlContent: htmlContent,
|
|
LastEditedBy: lastEditedBy,
|
|
ID: contentID,
|
|
SiteID: siteID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ContentItem{
|
|
ID: content.ID,
|
|
SiteID: content.SiteID,
|
|
HTMLContent: content.HtmlContent,
|
|
OriginalTemplate: FromNullString(content.OriginalTemplate),
|
|
UpdatedAt: fmt.Sprintf("%d", content.UpdatedAt),
|
|
LastEditedBy: content.LastEditedBy,
|
|
}, nil
|
|
}
|
|
|
|
// ReorderCollectionItems reorders collection items in bulk
|
|
func (r *PostgreSQLRepository) ReorderCollectionItems(ctx context.Context, siteID, collectionID string, items []CollectionItemPosition, lastEditedBy string) error {
|
|
// Use transaction for atomic bulk updates
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
qtx := r.queries.WithTx(tx)
|
|
for _, item := range items {
|
|
err = qtx.UpdateCollectionItemPosition(ctx, postgresql.UpdateCollectionItemPositionParams{
|
|
ItemID: item.ItemID,
|
|
CollectionID: collectionID,
|
|
SiteID: siteID,
|
|
Position: int32(item.Position),
|
|
LastEditedBy: lastEditedBy,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update position for item %s: %w", item.ItemID, err)
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// WithTransaction executes a function within a database transaction
|
|
func (r *PostgreSQLRepository) WithTransaction(ctx context.Context, fn func(ContentRepository) error) error {
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txRepo := &PostgreSQLRepository{
|
|
queries: r.queries.WithTx(tx),
|
|
db: r.db,
|
|
}
|
|
|
|
if err := fn(txRepo); err != nil {
|
|
tx.Rollback()
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|