Implement complete collection persistence with database-backed survival across server restarts

• Add full multi-table schema for collections with normalized design (collections, collection_templates, collection_items, collection_item_versions)
• Implement collection detection and processing in enhancement pipeline for .insertr-add elements
• Add template extraction and storage from existing HTML children with multi-variant support
• Enable collection reconstruction from database on server restart with proper DOM rebuilding
• Extend ContentClient interface with collection operations and full database integration
• Update enhance command to use engine.DatabaseClient for collection persistence support
This commit is contained in:
2025-09-22 18:29:58 +02:00
parent b25663f76b
commit 2315ba4750
36 changed files with 4356 additions and 46 deletions

View File

@@ -102,7 +102,7 @@ func runEnhance(cmd *cobra.Command, args []string) {
log.Fatalf("Failed to initialize database: %v", err) log.Fatalf("Failed to initialize database: %v", err)
} }
defer database.Close() defer database.Close()
client = content.NewDatabaseClient(database) client = engine.NewDatabaseClient(database)
} else { } else {
fmt.Printf("🧪 No database or API configured, using mock content\n") fmt.Printf("🧪 No database or API configured, using mock content\n")
client = content.NewMockClient() client = content.NewMockClient()

View File

@@ -95,6 +95,53 @@
font-size: 1.25rem; font-size: 1.25rem;
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
} }
/* Testimonials styling for .insertr-add demo */
.testimonials {
display: grid;
gap: 1rem;
margin: 1rem 0;
}
.testimonial-item {
background: white;
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 1.5rem;
position: relative;
}
.testimonial-item blockquote {
font-style: italic;
font-size: 1.1rem;
color: #374151;
margin: 0 0 1rem 0;
quotes: """ """;
}
.testimonial-item blockquote:before {
content: open-quote;
font-size: 1.5rem;
color: #3b82f6;
}
.testimonial-item blockquote:after {
content: close-quote;
font-size: 1.5rem;
color: #3b82f6;
}
.testimonial-item cite {
display: block;
text-align: right;
font-weight: 600;
color: #6b7280;
font-style: normal;
}
.testimonial-item cite:before {
content: "— ";
}
</style> </style>
</head> </head>
<body> <body>
@@ -166,5 +213,33 @@
</div> </div>
<p class="insertr">Need help? Contact our <a id="support-link" class="fancy" href="mailto:support@example.com" title="Email our support team">support team</a> anytime.</p> <p class="insertr">Need help? Contact our <a id="support-link" class="fancy" href="mailto:support@example.com" title="Email our support team">support team</a> anytime.</p>
</div> </div>
<!-- Example 8: Collection Management (.insertr-add) -->
<div class="example">
<h3>Example 8: Dynamic Collection (.insertr-add)</h3>
<div class="example-description">
Tests dynamic add/remove/reorder functionality. In edit mode, you should see "+ Add Item" button and item controls.
</div>
<div class="testimonials insertr-add">
<div class="testimonial-item">
<blockquote class="insertr">Not all that is gold does glitter</blockquote>
<cite class="insertr">Tolkien</cite>
</div>
<div class="testimonial-item">
<blockquote class="insertr">The journey of a thousand miles begins with one step</blockquote>
<cite class="insertr">Lao Tzu</cite>
</div>
<div class="testimonial-item">
<blockquote class="insertr">Innovation distinguishes between a leader and a follower</blockquote>
<cite class="insertr">Steve Jobs</cite>
</div>
</div>
</div>
<!-- Add login gate for testing -->
<footer style="margin-top: 3rem; padding: 2rem; border-top: 1px solid #e5e7eb; text-align: center;">
<p>&copy; 2024 Insertr Demo Site</p>
<a href="#" class="insertr-gate" style="color: #6b7280; text-decoration: none; font-size: 0.9rem;">Admin Login</a>
</footer>
</body> </body>
</html> </html>

View File

@@ -828,3 +828,129 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
order: -1; order: -1;
} }
} }
/* =================================================================
COLLECTION MANAGEMENT STYLES (.insertr-add)
================================================================= */
/* Collection container when active */
.insertr-collection-active {
outline: 2px dashed var(--insertr-primary);
outline-offset: 4px;
border-radius: var(--insertr-border-radius);
}
/* Add button positioned in top right of container */
.insertr-add-btn {
position: absolute;
top: -12px;
right: -12px;
background: var(--insertr-primary);
color: var(--insertr-text-inverse);
border: none;
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
border-radius: var(--insertr-border-radius);
font-size: var(--insertr-font-size-sm);
font-weight: 600;
cursor: pointer;
z-index: var(--insertr-z-floating);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.insertr-add-btn:hover {
background: var(--insertr-primary-hover);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.insertr-add-btn:active {
transform: translateY(0);
}
/* Item controls positioned in top right corner of each item */
.insertr-item-controls {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 2px;
opacity: 0;
transition: opacity 0.2s ease;
z-index: var(--insertr-z-floating);
}
/* Individual control buttons */
.insertr-control-btn {
width: 20px;
height: 20px;
background: var(--insertr-bg-primary);
border: 1px solid var(--insertr-border-color);
border-radius: 3px;
font-size: 12px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--insertr-text-primary);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.15s ease;
}
.insertr-control-btn:hover {
background: var(--insertr-bg-secondary);
border-color: var(--insertr-primary);
color: var(--insertr-primary);
transform: scale(1.1);
}
/* Remove button specific styling */
.insertr-control-btn:last-child {
color: var(--insertr-danger);
}
.insertr-control-btn:last-child:hover {
background: var(--insertr-danger);
color: var(--insertr-text-inverse);
border-color: var(--insertr-danger);
}
/* Collection items hover state */
.insertr-collection-active > *:hover {
background: rgba(0, 123, 255, 0.03);
outline: 1px solid rgba(var(--insertr-primary), 0.2);
outline-offset: 2px;
border-radius: var(--insertr-border-radius);
}
/* Show item controls on hover */
.insertr-collection-active > *:hover .insertr-item-controls {
opacity: 1;
}
/* Responsive adjustments for collection management */
@media (max-width: 768px) {
.insertr-add-btn {
position: static;
display: block;
margin: var(--insertr-spacing-sm) auto 0;
width: 100%;
max-width: 200px;
}
.insertr-item-controls {
position: relative;
opacity: 1;
top: auto;
right: auto;
justify-content: center;
margin-top: var(--insertr-spacing-xs);
}
.insertr-control-btn {
width: 32px;
height: 32px;
font-size: 14px;
}
}

View File

@@ -171,3 +171,20 @@ func (c *HTTPClient) CreateContent(siteID, contentID, htmlContent, originalTempl
// This would typically be used in API-driven enhancement scenarios // This would typically be used in API-driven enhancement scenarios
return nil, fmt.Errorf("CreateContent not implemented for HTTPClient - use DatabaseClient for enhancement") return nil, fmt.Errorf("CreateContent not implemented for HTTPClient - use DatabaseClient for enhancement")
} }
// Collection method stubs - TODO: Implement these for HTTP API
func (c *HTTPClient) GetCollection(siteID, collectionID string) (*engine.CollectionItem, error) {
return nil, fmt.Errorf("collection operations not implemented in HTTPClient")
}
func (c *HTTPClient) CreateCollection(siteID, collectionID, containerHTML, lastEditedBy string) (*engine.CollectionItem, error) {
return nil, fmt.Errorf("collection operations not implemented in HTTPClient")
}
func (c *HTTPClient) GetCollectionItems(siteID, collectionID string) ([]engine.CollectionItemWithTemplate, error) {
return nil, fmt.Errorf("collection operations not implemented in HTTPClient")
}
func (c *HTTPClient) CreateCollectionTemplate(siteID, collectionID, name, htmlTemplate string, isDefault bool) (*engine.CollectionTemplateItem, error) {
return nil, fmt.Errorf("collection operations not implemented in HTTPClient")
}

View File

@@ -233,3 +233,20 @@ func toNullString(s string) sql.NullString {
} }
return sql.NullString{String: s, Valid: true} return sql.NullString{String: s, Valid: true}
} }
// Collection method stubs - TODO: Implement these
func (d *DatabaseClient) GetCollection(siteID, collectionID string) (*engine.CollectionItem, error) {
return nil, fmt.Errorf("collection operations not implemented in content.DatabaseClient")
}
func (d *DatabaseClient) CreateCollection(siteID, collectionID, containerHTML, lastEditedBy string) (*engine.CollectionItem, error) {
return nil, fmt.Errorf("collection operations not implemented in content.DatabaseClient")
}
func (d *DatabaseClient) GetCollectionItems(siteID, collectionID string) ([]engine.CollectionItemWithTemplate, error) {
return nil, fmt.Errorf("collection operations not implemented in content.DatabaseClient")
}
func (d *DatabaseClient) CreateCollectionTemplate(siteID, collectionID, name, htmlTemplate string, isDefault bool) (*engine.CollectionTemplateItem, error) {
return nil, fmt.Errorf("collection operations not implemented in content.DatabaseClient")
}

View File

@@ -1,6 +1,7 @@
package content package content
import ( import (
"fmt"
"time" "time"
"github.com/insertr/insertr/internal/engine" "github.com/insertr/insertr/internal/engine"
@@ -156,3 +157,20 @@ func (m *MockClient) CreateContent(siteID, contentID, htmlContent, originalTempl
return &item, nil return &item, nil
} }
// Collection method stubs - TODO: Implement these for mock testing
func (m *MockClient) GetCollection(siteID, collectionID string) (*engine.CollectionItem, error) {
return nil, fmt.Errorf("collection operations not implemented in MockClient")
}
func (m *MockClient) CreateCollection(siteID, collectionID, containerHTML, lastEditedBy string) (*engine.CollectionItem, error) {
return nil, fmt.Errorf("collection operations not implemented in MockClient")
}
func (m *MockClient) GetCollectionItems(siteID, collectionID string) ([]engine.CollectionItemWithTemplate, error) {
return nil, fmt.Errorf("collection operations not implemented in MockClient")
}
func (m *MockClient) CreateCollectionTemplate(siteID, collectionID, name, htmlTemplate string, isDefault bool) (*engine.CollectionTemplateItem, error) {
return nil, fmt.Errorf("collection operations not implemented in MockClient")
}

View File

@@ -108,11 +108,36 @@ func (db *Database) initializeSQLiteSchema() error {
return fmt.Errorf("failed to create content_versions table: %w", err) return fmt.Errorf("failed to create content_versions table: %w", err)
} }
// Create collection tables
if err := db.sqliteQueries.InitializeCollectionsTable(ctx); err != nil {
return fmt.Errorf("failed to create collections table: %w", err)
}
if err := db.sqliteQueries.InitializeCollectionTemplatesTable(ctx); err != nil {
return fmt.Errorf("failed to create collection_templates table: %w", err)
}
if err := db.sqliteQueries.InitializeCollectionItemsTable(ctx); err != nil {
return fmt.Errorf("failed to create collection_items table: %w", err)
}
if err := db.sqliteQueries.InitializeCollectionItemVersionsTable(ctx); err != nil {
return fmt.Errorf("failed to create collection_item_versions table: %w", err)
}
// Create indexes manually (sqlc doesn't generate CREATE INDEX functions for SQLite) // Create indexes manually (sqlc doesn't generate CREATE INDEX functions for SQLite)
indexQueries := []string{ indexQueries := []string{
"CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id);", "CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id);",
"CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at);", "CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at);",
"CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC);", "CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC);",
"CREATE INDEX IF NOT EXISTS idx_collections_site_id ON collections(site_id);",
"CREATE INDEX IF NOT EXISTS idx_collections_updated_at ON collections(updated_at);",
"CREATE INDEX IF NOT EXISTS idx_collection_templates_lookup ON collection_templates(collection_id, site_id);",
"CREATE INDEX IF NOT EXISTS idx_collection_templates_default ON collection_templates(collection_id, site_id, is_default DESC);",
"CREATE INDEX IF NOT EXISTS idx_collection_items_lookup ON collection_items(collection_id, site_id, position);",
"CREATE INDEX IF NOT EXISTS idx_collection_items_template ON collection_items(template_id);",
"CREATE INDEX IF NOT EXISTS idx_collection_item_versions_lookup ON collection_item_versions(item_id, collection_id, site_id, created_at DESC);",
"CREATE UNIQUE INDEX IF NOT EXISTS idx_collection_templates_one_default ON collection_templates(collection_id, site_id) WHERE is_default = 1;",
} }
for _, query := range indexQueries { for _, query := range indexQueries {
@@ -121,17 +146,32 @@ func (db *Database) initializeSQLiteSchema() error {
} }
} }
// Create update trigger manually (sqlc doesn't generate trigger creation functions) // Create update triggers manually (sqlc doesn't generate trigger creation functions)
triggerQuery := ` triggerQueries := []string{
CREATE TRIGGER IF NOT EXISTS update_content_updated_at `CREATE TRIGGER IF NOT EXISTS update_content_updated_at
AFTER UPDATE ON content AFTER UPDATE ON content
FOR EACH ROW FOR EACH ROW
BEGIN BEGIN
UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id; UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id;
END;` END;`,
`CREATE TRIGGER IF NOT EXISTS update_collections_updated_at
AFTER UPDATE ON collections
FOR EACH ROW
BEGIN
UPDATE collections SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id;
END;`,
`CREATE TRIGGER IF NOT EXISTS update_collection_items_updated_at
AFTER UPDATE ON collection_items
FOR EACH ROW
BEGIN
UPDATE collection_items SET updated_at = strftime('%s', 'now') WHERE item_id = NEW.item_id AND collection_id = NEW.collection_id AND site_id = NEW.site_id;
END;`,
}
if _, err := db.conn.Exec(triggerQuery); err != nil { for _, query := range triggerQueries {
return fmt.Errorf("failed to create update trigger: %w", err) if _, err := db.conn.Exec(query); err != nil {
return fmt.Errorf("failed to create trigger: %w", err)
}
} }
return nil return nil
@@ -150,6 +190,23 @@ func (db *Database) initializePostgreSQLSchema() error {
return fmt.Errorf("failed to create content_versions table: %w", err) return fmt.Errorf("failed to create content_versions table: %w", err)
} }
// Create collection tables
if err := db.postgresqlQueries.InitializeCollectionsTable(ctx); err != nil {
return fmt.Errorf("failed to create collections table: %w", err)
}
if err := db.postgresqlQueries.InitializeCollectionTemplatesTable(ctx); err != nil {
return fmt.Errorf("failed to create collection_templates table: %w", err)
}
if err := db.postgresqlQueries.InitializeCollectionItemsTable(ctx); err != nil {
return fmt.Errorf("failed to create collection_items table: %w", err)
}
if err := db.postgresqlQueries.InitializeCollectionItemVersionsTable(ctx); err != nil {
return fmt.Errorf("failed to create collection_item_versions table: %w", err)
}
// Create indexes using sqlc-generated functions (PostgreSQL supports this) // Create indexes using sqlc-generated functions (PostgreSQL supports this)
if err := db.postgresqlQueries.CreateContentSiteIndex(ctx); err != nil { if err := db.postgresqlQueries.CreateContentSiteIndex(ctx); err != nil {
return fmt.Errorf("failed to create content site index: %w", err) return fmt.Errorf("failed to create content site index: %w", err)
@@ -163,21 +220,67 @@ func (db *Database) initializePostgreSQLSchema() error {
return fmt.Errorf("failed to create versions lookup index: %w", err) return fmt.Errorf("failed to create versions lookup index: %w", err)
} }
// Create collection indexes using sqlc-generated functions
if err := db.postgresqlQueries.CreateCollectionsSiteIndex(ctx); err != nil {
return fmt.Errorf("failed to create collections site index: %w", err)
}
if err := db.postgresqlQueries.CreateCollectionsUpdatedAtIndex(ctx); err != nil {
return fmt.Errorf("failed to create collections updated_at index: %w", err)
}
if err := db.postgresqlQueries.CreateCollectionTemplatesLookupIndex(ctx); err != nil {
return fmt.Errorf("failed to create collection templates lookup index: %w", err)
}
if err := db.postgresqlQueries.CreateCollectionTemplatesDefaultIndex(ctx); err != nil {
return fmt.Errorf("failed to create collection templates default index: %w", err)
}
if err := db.postgresqlQueries.CreateCollectionItemsLookupIndex(ctx); err != nil {
return fmt.Errorf("failed to create collection items lookup index: %w", err)
}
if err := db.postgresqlQueries.CreateCollectionItemsTemplateIndex(ctx); err != nil {
return fmt.Errorf("failed to create collection items template index: %w", err)
}
if err := db.postgresqlQueries.CreateCollectionItemVersionsLookupIndex(ctx); err != nil {
return fmt.Errorf("failed to create collection item versions lookup index: %w", err)
}
if err := db.postgresqlQueries.CreateCollectionTemplatesOneDefaultIndex(ctx); err != nil {
return fmt.Errorf("failed to create collection templates one default constraint: %w", err)
}
// Create update function using sqlc-generated function // Create update function using sqlc-generated function
if err := db.postgresqlQueries.CreateUpdateFunction(ctx); err != nil { if err := db.postgresqlQueries.CreateUpdateFunction(ctx); err != nil {
return fmt.Errorf("failed to create update function: %w", err) return fmt.Errorf("failed to create update function: %w", err)
} }
// Create trigger manually (sqlc doesn't generate trigger creation functions) // Create triggers manually (sqlc doesn't generate trigger creation functions)
triggerQuery := ` triggerQueries := []string{
DROP TRIGGER IF EXISTS update_content_updated_at ON content; `DROP TRIGGER IF EXISTS update_content_updated_at ON content;
CREATE TRIGGER update_content_updated_at CREATE TRIGGER update_content_updated_at
BEFORE UPDATE ON content BEFORE UPDATE ON content
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION update_content_timestamp();` EXECUTE FUNCTION update_content_timestamp();`,
`DROP TRIGGER IF EXISTS update_collections_updated_at ON collections;
CREATE TRIGGER update_collections_updated_at
BEFORE UPDATE ON collections
FOR EACH ROW
EXECUTE FUNCTION update_content_timestamp();`,
`DROP TRIGGER IF EXISTS update_collection_items_updated_at ON collection_items;
CREATE TRIGGER update_collection_items_updated_at
BEFORE UPDATE ON collection_items
FOR EACH ROW
EXECUTE FUNCTION update_content_timestamp();`,
}
if _, err := db.conn.Exec(triggerQuery); err != nil { for _, query := range triggerQueries {
return fmt.Errorf("failed to create update trigger: %w", err) if _, err := db.conn.Exec(query); err != nil {
return fmt.Errorf("failed to create trigger: %w", err)
}
} }
return nil return nil

View File

@@ -0,0 +1,197 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: collection_item_versions.sql
package postgresql
import (
"context"
"database/sql"
)
const createCollectionItemVersion = `-- name: CreateCollectionItemVersion :exec
INSERT INTO collection_item_versions (item_id, collection_id, site_id, html_content, template_id, position, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`
type CreateCollectionItemVersionParams struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
TemplateID int32 `json:"template_id"`
Position int32 `json:"position"`
CreatedBy string `json:"created_by"`
}
// Collection item versions table queries
func (q *Queries) CreateCollectionItemVersion(ctx context.Context, arg CreateCollectionItemVersionParams) error {
_, err := q.db.ExecContext(ctx, createCollectionItemVersion,
arg.ItemID,
arg.CollectionID,
arg.SiteID,
arg.HtmlContent,
arg.TemplateID,
arg.Position,
arg.CreatedBy,
)
return err
}
const deleteOldCollectionItemVersions = `-- name: DeleteOldCollectionItemVersions :exec
DELETE FROM collection_item_versions
WHERE created_at < $1 AND site_id = $2
`
type DeleteOldCollectionItemVersionsParams struct {
CreatedBefore int64 `json:"created_before"`
SiteID string `json:"site_id"`
}
func (q *Queries) DeleteOldCollectionItemVersions(ctx context.Context, arg DeleteOldCollectionItemVersionsParams) error {
_, err := q.db.ExecContext(ctx, deleteOldCollectionItemVersions, arg.CreatedBefore, arg.SiteID)
return err
}
const getAllCollectionItemVersionsForSite = `-- name: GetAllCollectionItemVersionsForSite :many
SELECT
civ.version_id, civ.item_id, civ.collection_id, civ.site_id, civ.html_content, civ.template_id, civ.position, civ.created_at, civ.created_by,
ci.html_content as current_html_content, ci.position as current_position
FROM collection_item_versions civ
LEFT JOIN collection_items ci ON civ.item_id = ci.item_id AND civ.collection_id = ci.collection_id AND civ.site_id = ci.site_id
WHERE civ.site_id = $1
ORDER BY civ.created_at DESC
LIMIT $2
`
type GetAllCollectionItemVersionsForSiteParams struct {
SiteID string `json:"site_id"`
LimitCount int32 `json:"limit_count"`
}
type GetAllCollectionItemVersionsForSiteRow struct {
VersionID int32 `json:"version_id"`
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
TemplateID int32 `json:"template_id"`
Position int32 `json:"position"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
CurrentHtmlContent sql.NullString `json:"current_html_content"`
CurrentPosition sql.NullInt32 `json:"current_position"`
}
func (q *Queries) GetAllCollectionItemVersionsForSite(ctx context.Context, arg GetAllCollectionItemVersionsForSiteParams) ([]GetAllCollectionItemVersionsForSiteRow, error) {
rows, err := q.db.QueryContext(ctx, getAllCollectionItemVersionsForSite, arg.SiteID, arg.LimitCount)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllCollectionItemVersionsForSiteRow
for rows.Next() {
var i GetAllCollectionItemVersionsForSiteRow
if err := rows.Scan(
&i.VersionID,
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.HtmlContent,
&i.TemplateID,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
&i.CurrentHtmlContent,
&i.CurrentPosition,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getCollectionItemVersion = `-- name: GetCollectionItemVersion :one
SELECT version_id, item_id, collection_id, site_id, html_content, template_id, position, created_at, created_by
FROM collection_item_versions
WHERE version_id = $1
`
func (q *Queries) GetCollectionItemVersion(ctx context.Context, versionID int32) (CollectionItemVersion, error) {
row := q.db.QueryRowContext(ctx, getCollectionItemVersion, versionID)
var i CollectionItemVersion
err := row.Scan(
&i.VersionID,
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.HtmlContent,
&i.TemplateID,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
)
return i, err
}
const getCollectionItemVersionHistory = `-- name: GetCollectionItemVersionHistory :many
SELECT version_id, item_id, collection_id, site_id, html_content, template_id, position, created_at, created_by
FROM collection_item_versions
WHERE item_id = $1 AND collection_id = $2 AND site_id = $3
ORDER BY created_at DESC
LIMIT $4
`
type GetCollectionItemVersionHistoryParams struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
LimitCount int32 `json:"limit_count"`
}
func (q *Queries) GetCollectionItemVersionHistory(ctx context.Context, arg GetCollectionItemVersionHistoryParams) ([]CollectionItemVersion, error) {
rows, err := q.db.QueryContext(ctx, getCollectionItemVersionHistory,
arg.ItemID,
arg.CollectionID,
arg.SiteID,
arg.LimitCount,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CollectionItemVersion
for rows.Next() {
var i CollectionItemVersion
if err := rows.Scan(
&i.VersionID,
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.HtmlContent,
&i.TemplateID,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@@ -0,0 +1,327 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: collection_items.sql
package postgresql
import (
"context"
)
const createCollectionItem = `-- name: CreateCollectionItem :one
INSERT INTO collection_items (item_id, collection_id, site_id, template_id, html_content, position, last_edited_by)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING item_id, collection_id, site_id, template_id, html_content, position, created_at, updated_at, last_edited_by
`
type CreateCollectionItemParams struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
TemplateID int32 `json:"template_id"`
HtmlContent string `json:"html_content"`
Position int32 `json:"position"`
LastEditedBy string `json:"last_edited_by"`
}
func (q *Queries) CreateCollectionItem(ctx context.Context, arg CreateCollectionItemParams) (CollectionItem, error) {
row := q.db.QueryRowContext(ctx, createCollectionItem,
arg.ItemID,
arg.CollectionID,
arg.SiteID,
arg.TemplateID,
arg.HtmlContent,
arg.Position,
arg.LastEditedBy,
)
var i CollectionItem
err := row.Scan(
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.TemplateID,
&i.HtmlContent,
&i.Position,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const deleteCollectionItem = `-- name: DeleteCollectionItem :exec
DELETE FROM collection_items
WHERE item_id = $1 AND collection_id = $2 AND site_id = $3
`
type DeleteCollectionItemParams struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) DeleteCollectionItem(ctx context.Context, arg DeleteCollectionItemParams) error {
_, err := q.db.ExecContext(ctx, deleteCollectionItem, arg.ItemID, arg.CollectionID, arg.SiteID)
return err
}
const deleteCollectionItems = `-- name: DeleteCollectionItems :exec
DELETE FROM collection_items
WHERE collection_id = $1 AND site_id = $2
`
type DeleteCollectionItemsParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) DeleteCollectionItems(ctx context.Context, arg DeleteCollectionItemsParams) error {
_, err := q.db.ExecContext(ctx, deleteCollectionItems, arg.CollectionID, arg.SiteID)
return err
}
const getCollectionItem = `-- name: GetCollectionItem :one
SELECT item_id, collection_id, site_id, template_id, html_content, position, created_at, updated_at, last_edited_by
FROM collection_items
WHERE item_id = $1 AND collection_id = $2 AND site_id = $3
`
type GetCollectionItemParams struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
// Collection items table queries
func (q *Queries) GetCollectionItem(ctx context.Context, arg GetCollectionItemParams) (CollectionItem, error) {
row := q.db.QueryRowContext(ctx, getCollectionItem, arg.ItemID, arg.CollectionID, arg.SiteID)
var i CollectionItem
err := row.Scan(
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.TemplateID,
&i.HtmlContent,
&i.Position,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const getCollectionItems = `-- name: GetCollectionItems :many
SELECT item_id, collection_id, site_id, template_id, html_content, position, created_at, updated_at, last_edited_by
FROM collection_items
WHERE collection_id = $1 AND site_id = $2
ORDER BY position ASC
`
type GetCollectionItemsParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) GetCollectionItems(ctx context.Context, arg GetCollectionItemsParams) ([]CollectionItem, error) {
rows, err := q.db.QueryContext(ctx, getCollectionItems, arg.CollectionID, arg.SiteID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CollectionItem
for rows.Next() {
var i CollectionItem
if err := rows.Scan(
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.TemplateID,
&i.HtmlContent,
&i.Position,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getCollectionItemsWithTemplate = `-- name: GetCollectionItemsWithTemplate :many
SELECT
ci.item_id, ci.collection_id, ci.site_id, ci.template_id, ci.html_content, ci.position, ci.created_at, ci.updated_at, ci.last_edited_by,
ct.name as template_name, ct.html_template, ct.is_default
FROM collection_items ci
JOIN collection_templates ct ON ci.template_id = ct.template_id
WHERE ci.collection_id = $1 AND ci.site_id = $2
ORDER BY ci.position ASC
`
type GetCollectionItemsWithTemplateParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
type GetCollectionItemsWithTemplateRow struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
TemplateID int32 `json:"template_id"`
HtmlContent string `json:"html_content"`
Position int32 `json:"position"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
TemplateName string `json:"template_name"`
HtmlTemplate string `json:"html_template"`
IsDefault bool `json:"is_default"`
}
func (q *Queries) GetCollectionItemsWithTemplate(ctx context.Context, arg GetCollectionItemsWithTemplateParams) ([]GetCollectionItemsWithTemplateRow, error) {
rows, err := q.db.QueryContext(ctx, getCollectionItemsWithTemplate, arg.CollectionID, arg.SiteID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetCollectionItemsWithTemplateRow
for rows.Next() {
var i GetCollectionItemsWithTemplateRow
if err := rows.Scan(
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.TemplateID,
&i.HtmlContent,
&i.Position,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
&i.TemplateName,
&i.HtmlTemplate,
&i.IsDefault,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getMaxPosition = `-- name: GetMaxPosition :one
SELECT COALESCE(MAX(position), 0) as max_position
FROM collection_items
WHERE collection_id = $1 AND site_id = $2
`
type GetMaxPositionParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) GetMaxPosition(ctx context.Context, arg GetMaxPositionParams) (interface{}, error) {
row := q.db.QueryRowContext(ctx, getMaxPosition, arg.CollectionID, arg.SiteID)
var max_position interface{}
err := row.Scan(&max_position)
return max_position, err
}
const reorderCollectionItems = `-- name: ReorderCollectionItems :exec
UPDATE collection_items
SET position = position + $1
WHERE collection_id = $2 AND site_id = $3
AND position >= $4
`
type ReorderCollectionItemsParams struct {
PositionDelta int32 `json:"position_delta"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
StartPosition int32 `json:"start_position"`
}
func (q *Queries) ReorderCollectionItems(ctx context.Context, arg ReorderCollectionItemsParams) error {
_, err := q.db.ExecContext(ctx, reorderCollectionItems,
arg.PositionDelta,
arg.CollectionID,
arg.SiteID,
arg.StartPosition,
)
return err
}
const updateCollectionItem = `-- name: UpdateCollectionItem :one
UPDATE collection_items
SET html_content = $1, last_edited_by = $2
WHERE item_id = $3 AND collection_id = $4 AND site_id = $5
RETURNING item_id, collection_id, site_id, template_id, html_content, position, created_at, updated_at, last_edited_by
`
type UpdateCollectionItemParams struct {
HtmlContent string `json:"html_content"`
LastEditedBy string `json:"last_edited_by"`
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) UpdateCollectionItem(ctx context.Context, arg UpdateCollectionItemParams) (CollectionItem, error) {
row := q.db.QueryRowContext(ctx, updateCollectionItem,
arg.HtmlContent,
arg.LastEditedBy,
arg.ItemID,
arg.CollectionID,
arg.SiteID,
)
var i CollectionItem
err := row.Scan(
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.TemplateID,
&i.HtmlContent,
&i.Position,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const updateCollectionItemPosition = `-- name: UpdateCollectionItemPosition :exec
UPDATE collection_items
SET position = $1
WHERE item_id = $2 AND collection_id = $3 AND site_id = $4
`
type UpdateCollectionItemPositionParams struct {
Position int32 `json:"position"`
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) UpdateCollectionItemPosition(ctx context.Context, arg UpdateCollectionItemPositionParams) error {
_, err := q.db.ExecContext(ctx, updateCollectionItemPosition,
arg.Position,
arg.ItemID,
arg.CollectionID,
arg.SiteID,
)
return err
}

View File

@@ -0,0 +1,217 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: collection_templates.sql
package postgresql
import (
"context"
)
const createCollectionTemplate = `-- name: CreateCollectionTemplate :one
INSERT INTO collection_templates (collection_id, site_id, name, html_template, is_default)
VALUES ($1, $2, $3, $4, $5)
RETURNING template_id, collection_id, site_id, name, html_template, is_default, created_at
`
type CreateCollectionTemplateParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
Name string `json:"name"`
HtmlTemplate string `json:"html_template"`
IsDefault bool `json:"is_default"`
}
func (q *Queries) CreateCollectionTemplate(ctx context.Context, arg CreateCollectionTemplateParams) (CollectionTemplate, error) {
row := q.db.QueryRowContext(ctx, createCollectionTemplate,
arg.CollectionID,
arg.SiteID,
arg.Name,
arg.HtmlTemplate,
arg.IsDefault,
)
var i CollectionTemplate
err := row.Scan(
&i.TemplateID,
&i.CollectionID,
&i.SiteID,
&i.Name,
&i.HtmlTemplate,
&i.IsDefault,
&i.CreatedAt,
)
return i, err
}
const deleteCollectionTemplate = `-- name: DeleteCollectionTemplate :exec
DELETE FROM collection_templates
WHERE template_id = $1
`
func (q *Queries) DeleteCollectionTemplate(ctx context.Context, templateID int32) error {
_, err := q.db.ExecContext(ctx, deleteCollectionTemplate, templateID)
return err
}
const deleteCollectionTemplates = `-- name: DeleteCollectionTemplates :exec
DELETE FROM collection_templates
WHERE collection_id = $1 AND site_id = $2
`
type DeleteCollectionTemplatesParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) DeleteCollectionTemplates(ctx context.Context, arg DeleteCollectionTemplatesParams) error {
_, err := q.db.ExecContext(ctx, deleteCollectionTemplates, arg.CollectionID, arg.SiteID)
return err
}
const getCollectionTemplate = `-- name: GetCollectionTemplate :one
SELECT template_id, collection_id, site_id, name, html_template, is_default, created_at
FROM collection_templates
WHERE template_id = $1
`
// Collection templates table queries
func (q *Queries) GetCollectionTemplate(ctx context.Context, templateID int32) (CollectionTemplate, error) {
row := q.db.QueryRowContext(ctx, getCollectionTemplate, templateID)
var i CollectionTemplate
err := row.Scan(
&i.TemplateID,
&i.CollectionID,
&i.SiteID,
&i.Name,
&i.HtmlTemplate,
&i.IsDefault,
&i.CreatedAt,
)
return i, err
}
const getCollectionTemplates = `-- name: GetCollectionTemplates :many
SELECT template_id, collection_id, site_id, name, html_template, is_default, created_at
FROM collection_templates
WHERE collection_id = $1 AND site_id = $2
ORDER BY is_default DESC, created_at ASC
`
type GetCollectionTemplatesParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) GetCollectionTemplates(ctx context.Context, arg GetCollectionTemplatesParams) ([]CollectionTemplate, error) {
rows, err := q.db.QueryContext(ctx, getCollectionTemplates, arg.CollectionID, arg.SiteID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CollectionTemplate
for rows.Next() {
var i CollectionTemplate
if err := rows.Scan(
&i.TemplateID,
&i.CollectionID,
&i.SiteID,
&i.Name,
&i.HtmlTemplate,
&i.IsDefault,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getDefaultTemplate = `-- name: GetDefaultTemplate :one
SELECT template_id, collection_id, site_id, name, html_template, is_default, created_at
FROM collection_templates
WHERE collection_id = $1 AND site_id = $2 AND is_default = TRUE
LIMIT 1
`
type GetDefaultTemplateParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) GetDefaultTemplate(ctx context.Context, arg GetDefaultTemplateParams) (CollectionTemplate, error) {
row := q.db.QueryRowContext(ctx, getDefaultTemplate, arg.CollectionID, arg.SiteID)
var i CollectionTemplate
err := row.Scan(
&i.TemplateID,
&i.CollectionID,
&i.SiteID,
&i.Name,
&i.HtmlTemplate,
&i.IsDefault,
&i.CreatedAt,
)
return i, err
}
const setTemplateAsDefault = `-- name: SetTemplateAsDefault :exec
UPDATE collection_templates
SET is_default = CASE
WHEN template_id = $1 THEN TRUE
ELSE FALSE
END
WHERE collection_id = $2 AND site_id = $3
`
type SetTemplateAsDefaultParams struct {
TemplateID int32 `json:"template_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) SetTemplateAsDefault(ctx context.Context, arg SetTemplateAsDefaultParams) error {
_, err := q.db.ExecContext(ctx, setTemplateAsDefault, arg.TemplateID, arg.CollectionID, arg.SiteID)
return err
}
const updateCollectionTemplate = `-- name: UpdateCollectionTemplate :one
UPDATE collection_templates
SET name = $1, html_template = $2, is_default = $3
WHERE template_id = $4
RETURNING template_id, collection_id, site_id, name, html_template, is_default, created_at
`
type UpdateCollectionTemplateParams struct {
Name string `json:"name"`
HtmlTemplate string `json:"html_template"`
IsDefault bool `json:"is_default"`
TemplateID int32 `json:"template_id"`
}
func (q *Queries) UpdateCollectionTemplate(ctx context.Context, arg UpdateCollectionTemplateParams) (CollectionTemplate, error) {
row := q.db.QueryRowContext(ctx, updateCollectionTemplate,
arg.Name,
arg.HtmlTemplate,
arg.IsDefault,
arg.TemplateID,
)
var i CollectionTemplate
err := row.Scan(
&i.TemplateID,
&i.CollectionID,
&i.SiteID,
&i.Name,
&i.HtmlTemplate,
&i.IsDefault,
&i.CreatedAt,
)
return i, err
}

View File

@@ -0,0 +1,199 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: collections.sql
package postgresql
import (
"context"
)
const createCollection = `-- name: CreateCollection :one
INSERT INTO collections (id, site_id, container_html, last_edited_by)
VALUES ($1, $2, $3, $4)
RETURNING id, site_id, container_html, created_at, updated_at, last_edited_by
`
type CreateCollectionParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
ContainerHtml string `json:"container_html"`
LastEditedBy string `json:"last_edited_by"`
}
func (q *Queries) CreateCollection(ctx context.Context, arg CreateCollectionParams) (Collection, error) {
row := q.db.QueryRowContext(ctx, createCollection,
arg.ID,
arg.SiteID,
arg.ContainerHtml,
arg.LastEditedBy,
)
var i Collection
err := row.Scan(
&i.ID,
&i.SiteID,
&i.ContainerHtml,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const deleteAllSiteCollections = `-- name: DeleteAllSiteCollections :exec
DELETE FROM collections
WHERE site_id = $1
`
func (q *Queries) DeleteAllSiteCollections(ctx context.Context, siteID string) error {
_, err := q.db.ExecContext(ctx, deleteAllSiteCollections, siteID)
return err
}
const deleteCollection = `-- name: DeleteCollection :exec
DELETE FROM collections
WHERE id = $1 AND site_id = $2
`
type DeleteCollectionParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
}
func (q *Queries) DeleteCollection(ctx context.Context, arg DeleteCollectionParams) error {
_, err := q.db.ExecContext(ctx, deleteCollection, arg.ID, arg.SiteID)
return err
}
const getAllCollections = `-- name: GetAllCollections :many
SELECT id, site_id, container_html, created_at, updated_at, last_edited_by
FROM collections
WHERE site_id = $1
ORDER BY updated_at DESC
`
func (q *Queries) GetAllCollections(ctx context.Context, siteID string) ([]Collection, error) {
rows, err := q.db.QueryContext(ctx, getAllCollections, siteID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Collection
for rows.Next() {
var i Collection
if err := rows.Scan(
&i.ID,
&i.SiteID,
&i.ContainerHtml,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getCollection = `-- name: GetCollection :one
SELECT id, site_id, container_html, created_at, updated_at, last_edited_by
FROM collections
WHERE id = $1 AND site_id = $2
`
type GetCollectionParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
}
// Collections table queries
func (q *Queries) GetCollection(ctx context.Context, arg GetCollectionParams) (Collection, error) {
row := q.db.QueryRowContext(ctx, getCollection, arg.ID, arg.SiteID)
var i Collection
err := row.Scan(
&i.ID,
&i.SiteID,
&i.ContainerHtml,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const updateCollection = `-- name: UpdateCollection :one
UPDATE collections
SET container_html = $1, last_edited_by = $2
WHERE id = $3 AND site_id = $4
RETURNING id, site_id, container_html, created_at, updated_at, last_edited_by
`
type UpdateCollectionParams struct {
ContainerHtml string `json:"container_html"`
LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"`
SiteID string `json:"site_id"`
}
func (q *Queries) UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (Collection, error) {
row := q.db.QueryRowContext(ctx, updateCollection,
arg.ContainerHtml,
arg.LastEditedBy,
arg.ID,
arg.SiteID,
)
var i Collection
err := row.Scan(
&i.ID,
&i.SiteID,
&i.ContainerHtml,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const upsertCollection = `-- name: UpsertCollection :one
INSERT INTO collections (id, site_id, container_html, last_edited_by)
VALUES ($1, $2, $3, $4)
ON CONFLICT(id, site_id) DO UPDATE SET
container_html = EXCLUDED.container_html,
last_edited_by = EXCLUDED.last_edited_by
RETURNING id, site_id, container_html, created_at, updated_at, last_edited_by
`
type UpsertCollectionParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
ContainerHtml string `json:"container_html"`
LastEditedBy string `json:"last_edited_by"`
}
func (q *Queries) UpsertCollection(ctx context.Context, arg UpsertCollectionParams) (Collection, error) {
row := q.db.QueryRowContext(ctx, upsertCollection,
arg.ID,
arg.SiteID,
arg.ContainerHtml,
arg.LastEditedBy,
)
var i Collection
err := row.Scan(
&i.ID,
&i.SiteID,
&i.ContainerHtml,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}

View File

@@ -8,6 +8,49 @@ import (
"database/sql" "database/sql"
) )
type Collection struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
ContainerHtml string `json:"container_html"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
}
type CollectionItem struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
TemplateID int32 `json:"template_id"`
HtmlContent string `json:"html_content"`
Position int32 `json:"position"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
}
type CollectionItemVersion struct {
VersionID int32 `json:"version_id"`
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
TemplateID int32 `json:"template_id"`
Position int32 `json:"position"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
}
type CollectionTemplate struct {
TemplateID int32 `json:"template_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
Name string `json:"name"`
HtmlTemplate string `json:"html_template"`
IsDefault bool `json:"is_default"`
CreatedAt int64 `json:"created_at"`
}
type Content struct { type Content struct {
ID string `json:"id"` ID string `json:"id"`
SiteID string `json:"site_id"` SiteID string `json:"site_id"`

View File

@@ -9,24 +9,70 @@ import (
) )
type Querier interface { type Querier interface {
CreateCollection(ctx context.Context, arg CreateCollectionParams) (Collection, error)
CreateCollectionItem(ctx context.Context, arg CreateCollectionItemParams) (CollectionItem, error)
// Collection item versions table queries
CreateCollectionItemVersion(ctx context.Context, arg CreateCollectionItemVersionParams) error
CreateCollectionItemVersionsLookupIndex(ctx context.Context) error
CreateCollectionItemsLookupIndex(ctx context.Context) error
CreateCollectionItemsTemplateIndex(ctx context.Context) error
CreateCollectionTemplate(ctx context.Context, arg CreateCollectionTemplateParams) (CollectionTemplate, error)
CreateCollectionTemplatesDefaultIndex(ctx context.Context) error
CreateCollectionTemplatesLookupIndex(ctx context.Context) error
CreateCollectionTemplatesOneDefaultIndex(ctx context.Context) error
CreateCollectionsSiteIndex(ctx context.Context) error
CreateCollectionsUpdatedAtIndex(ctx context.Context) error
CreateContent(ctx context.Context, arg CreateContentParams) (Content, error) CreateContent(ctx context.Context, arg CreateContentParams) (Content, error)
CreateContentSiteIndex(ctx context.Context) error CreateContentSiteIndex(ctx context.Context) error
CreateContentUpdatedAtIndex(ctx context.Context) error CreateContentUpdatedAtIndex(ctx context.Context) error
CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error
CreateUpdateFunction(ctx context.Context) error CreateUpdateFunction(ctx context.Context) error
CreateVersionsLookupIndex(ctx context.Context) error CreateVersionsLookupIndex(ctx context.Context) error
DeleteAllSiteCollections(ctx context.Context, siteID string) error
DeleteAllSiteContent(ctx context.Context, siteID string) error DeleteAllSiteContent(ctx context.Context, siteID string) error
DeleteCollection(ctx context.Context, arg DeleteCollectionParams) error
DeleteCollectionItem(ctx context.Context, arg DeleteCollectionItemParams) error
DeleteCollectionItems(ctx context.Context, arg DeleteCollectionItemsParams) error
DeleteCollectionTemplate(ctx context.Context, templateID int32) error
DeleteCollectionTemplates(ctx context.Context, arg DeleteCollectionTemplatesParams) error
DeleteContent(ctx context.Context, arg DeleteContentParams) error DeleteContent(ctx context.Context, arg DeleteContentParams) error
DeleteOldCollectionItemVersions(ctx context.Context, arg DeleteOldCollectionItemVersionsParams) error
DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsParams) error DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsParams) error
GetAllCollectionItemVersionsForSite(ctx context.Context, arg GetAllCollectionItemVersionsForSiteParams) ([]GetAllCollectionItemVersionsForSiteRow, error)
GetAllCollections(ctx context.Context, siteID string) ([]Collection, error)
GetAllContent(ctx context.Context, siteID string) ([]Content, error) GetAllContent(ctx context.Context, siteID string) ([]Content, error)
GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsForSiteParams) ([]GetAllVersionsForSiteRow, error) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsForSiteParams) ([]GetAllVersionsForSiteRow, error)
GetBulkContent(ctx context.Context, arg GetBulkContentParams) ([]Content, error) GetBulkContent(ctx context.Context, arg GetBulkContentParams) ([]Content, error)
// Collections table queries
GetCollection(ctx context.Context, arg GetCollectionParams) (Collection, error)
// Collection items table queries
GetCollectionItem(ctx context.Context, arg GetCollectionItemParams) (CollectionItem, error)
GetCollectionItemVersion(ctx context.Context, versionID int32) (CollectionItemVersion, error)
GetCollectionItemVersionHistory(ctx context.Context, arg GetCollectionItemVersionHistoryParams) ([]CollectionItemVersion, error)
GetCollectionItems(ctx context.Context, arg GetCollectionItemsParams) ([]CollectionItem, error)
GetCollectionItemsWithTemplate(ctx context.Context, arg GetCollectionItemsWithTemplateParams) ([]GetCollectionItemsWithTemplateRow, error)
// Collection templates table queries
GetCollectionTemplate(ctx context.Context, templateID int32) (CollectionTemplate, error)
GetCollectionTemplates(ctx context.Context, arg GetCollectionTemplatesParams) ([]CollectionTemplate, error)
GetContent(ctx context.Context, arg GetContentParams) (Content, error) GetContent(ctx context.Context, arg GetContentParams) (Content, error)
GetContentVersion(ctx context.Context, versionID int32) (ContentVersion, error) GetContentVersion(ctx context.Context, versionID int32) (ContentVersion, error)
GetContentVersionHistory(ctx context.Context, arg GetContentVersionHistoryParams) ([]ContentVersion, error) GetContentVersionHistory(ctx context.Context, arg GetContentVersionHistoryParams) ([]ContentVersion, error)
GetDefaultTemplate(ctx context.Context, arg GetDefaultTemplateParams) (CollectionTemplate, error)
GetMaxPosition(ctx context.Context, arg GetMaxPositionParams) (interface{}, error)
InitializeCollectionItemVersionsTable(ctx context.Context) error
InitializeCollectionItemsTable(ctx context.Context) error
InitializeCollectionTemplatesTable(ctx context.Context) error
InitializeCollectionsTable(ctx context.Context) error
InitializeSchema(ctx context.Context) error InitializeSchema(ctx context.Context) error
InitializeVersionsTable(ctx context.Context) error InitializeVersionsTable(ctx context.Context) error
ReorderCollectionItems(ctx context.Context, arg ReorderCollectionItemsParams) error
SetTemplateAsDefault(ctx context.Context, arg SetTemplateAsDefaultParams) error
UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (Collection, error)
UpdateCollectionItem(ctx context.Context, arg UpdateCollectionItemParams) (CollectionItem, error)
UpdateCollectionItemPosition(ctx context.Context, arg UpdateCollectionItemPositionParams) error
UpdateCollectionTemplate(ctx context.Context, arg UpdateCollectionTemplateParams) (CollectionTemplate, error)
UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error) UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error)
UpsertCollection(ctx context.Context, arg UpsertCollectionParams) (Collection, error)
UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error)
} }

View File

@@ -22,11 +22,78 @@ CREATE TABLE content_versions (
created_by TEXT DEFAULT 'system' NOT NULL created_by TEXT DEFAULT 'system' NOT NULL
); );
-- Collections table - manages .insertr-add containers
CREATE TABLE collections (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
container_html TEXT NOT NULL,
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
);
-- Collection templates - multiple template variants per collection
CREATE TABLE collection_templates (
template_id SERIAL PRIMARY KEY,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
name TEXT NOT NULL,
html_template TEXT NOT NULL,
is_default BOOLEAN DEFAULT FALSE NOT NULL,
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
FOREIGN KEY (collection_id, site_id) REFERENCES collections(id, site_id) ON DELETE CASCADE
);
-- Collection items - individual items within collections
CREATE TABLE collection_items (
item_id TEXT NOT NULL,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
template_id INTEGER NOT NULL,
html_content TEXT NOT NULL,
position INTEGER NOT NULL,
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (item_id, collection_id, site_id),
FOREIGN KEY (collection_id, site_id) REFERENCES collections(id, site_id) ON DELETE CASCADE,
FOREIGN KEY (template_id) REFERENCES collection_templates(template_id) ON DELETE RESTRICT
);
-- Collection item version history
CREATE TABLE collection_item_versions (
version_id SERIAL PRIMARY KEY,
item_id TEXT NOT NULL,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
html_content TEXT NOT NULL,
template_id INTEGER NOT NULL,
position INTEGER NOT NULL,
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL,
FOREIGN KEY (item_id, collection_id, site_id) REFERENCES collection_items(item_id, collection_id, site_id) ON DELETE CASCADE
);
-- Indexes for performance -- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id); CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id);
CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at); CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at);
CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC);
-- Collection indexes for performance
CREATE INDEX IF NOT EXISTS idx_collections_site_id ON collections(site_id);
CREATE INDEX IF NOT EXISTS idx_collections_updated_at ON collections(updated_at);
CREATE INDEX IF NOT EXISTS idx_collection_templates_lookup ON collection_templates(collection_id, site_id);
CREATE INDEX IF NOT EXISTS idx_collection_templates_default ON collection_templates(collection_id, site_id, is_default DESC);
CREATE INDEX IF NOT EXISTS idx_collection_items_lookup ON collection_items(collection_id, site_id, position);
CREATE INDEX IF NOT EXISTS idx_collection_items_template ON collection_items(template_id);
CREATE INDEX IF NOT EXISTS idx_collection_item_versions_lookup ON collection_item_versions(item_id, collection_id, site_id, created_at DESC);
-- Constraint to ensure only one default template per collection
CREATE UNIQUE INDEX IF NOT EXISTS idx_collection_templates_one_default
ON collection_templates(collection_id, site_id)
WHERE is_default = TRUE;
-- Function and trigger to automatically update updated_at timestamp -- Function and trigger to automatically update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column() CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$ RETURNS TRIGGER AS $$
@@ -40,3 +107,14 @@ CREATE TRIGGER update_content_updated_at
BEFORE UPDATE ON content BEFORE UPDATE ON content
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column(); EXECUTE FUNCTION update_updated_at_column();
-- Triggers for collection timestamps
CREATE TRIGGER update_collections_updated_at
BEFORE UPDATE ON collections
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_collection_items_updated_at
BEFORE UPDATE ON collection_items
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

View File

@@ -21,6 +21,59 @@ CREATE TABLE IF NOT EXISTS content_versions (
created_by TEXT DEFAULT 'system' NOT NULL created_by TEXT DEFAULT 'system' NOT NULL
); );
-- name: InitializeCollectionsTable :exec
CREATE TABLE IF NOT EXISTS collections (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
container_html TEXT NOT NULL,
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
updated_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
);
-- name: InitializeCollectionTemplatesTable :exec
CREATE TABLE IF NOT EXISTS collection_templates (
template_id SERIAL PRIMARY KEY,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
name TEXT NOT NULL,
html_template TEXT NOT NULL,
is_default BOOLEAN DEFAULT FALSE NOT NULL,
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
FOREIGN KEY (collection_id, site_id) REFERENCES collections(id, site_id) ON DELETE CASCADE
);
-- name: InitializeCollectionItemsTable :exec
CREATE TABLE IF NOT EXISTS collection_items (
item_id TEXT NOT NULL,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
template_id INTEGER NOT NULL,
html_content TEXT NOT NULL,
position INTEGER NOT NULL,
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
updated_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (item_id, collection_id, site_id),
FOREIGN KEY (collection_id, site_id) REFERENCES collections(id, site_id) ON DELETE CASCADE,
FOREIGN KEY (template_id) REFERENCES collection_templates(template_id) ON DELETE RESTRICT
);
-- name: InitializeCollectionItemVersionsTable :exec
CREATE TABLE IF NOT EXISTS collection_item_versions (
version_id SERIAL PRIMARY KEY,
item_id TEXT NOT NULL,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
html_content TEXT NOT NULL,
template_id INTEGER NOT NULL,
position INTEGER NOT NULL,
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL,
FOREIGN KEY (item_id, collection_id, site_id) REFERENCES collection_items(item_id, collection_id, site_id) ON DELETE CASCADE
);
-- name: CreateContentSiteIndex :exec -- name: CreateContentSiteIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id); CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id);
@@ -30,6 +83,32 @@ CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at);
-- name: CreateVersionsLookupIndex :exec -- name: CreateVersionsLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC);
-- name: CreateCollectionsSiteIndex :exec
CREATE INDEX IF NOT EXISTS idx_collections_site_id ON collections(site_id);
-- name: CreateCollectionsUpdatedAtIndex :exec
CREATE INDEX IF NOT EXISTS idx_collections_updated_at ON collections(updated_at);
-- name: CreateCollectionTemplatesLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_templates_lookup ON collection_templates(collection_id, site_id);
-- name: CreateCollectionTemplatesDefaultIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_templates_default ON collection_templates(collection_id, site_id, is_default DESC);
-- name: CreateCollectionItemsLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_items_lookup ON collection_items(collection_id, site_id, position);
-- name: CreateCollectionItemsTemplateIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_items_template ON collection_items(template_id);
-- name: CreateCollectionItemVersionsLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_item_versions_lookup ON collection_item_versions(item_id, collection_id, site_id, created_at DESC);
-- name: CreateCollectionTemplatesOneDefaultIndex :exec
CREATE UNIQUE INDEX IF NOT EXISTS idx_collection_templates_one_default
ON collection_templates(collection_id, site_id)
WHERE is_default = TRUE;
-- name: CreateUpdateFunction :exec -- name: CreateUpdateFunction :exec
CREATE OR REPLACE FUNCTION update_content_timestamp() CREATE OR REPLACE FUNCTION update_content_timestamp()
RETURNS TRIGGER AS $$ RETURNS TRIGGER AS $$
@@ -45,3 +124,17 @@ CREATE TRIGGER update_content_updated_at
BEFORE UPDATE ON content BEFORE UPDATE ON content
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION update_content_timestamp(); EXECUTE FUNCTION update_content_timestamp();
-- name: CreateCollectionsUpdateTrigger :exec
DROP TRIGGER IF EXISTS update_collections_updated_at ON collections;
CREATE TRIGGER update_collections_updated_at
BEFORE UPDATE ON collections
FOR EACH ROW
EXECUTE FUNCTION update_content_timestamp();
-- name: CreateCollectionItemsUpdateTrigger :exec
DROP TRIGGER IF EXISTS update_collection_items_updated_at ON collection_items;
CREATE TRIGGER update_collection_items_updated_at
BEFORE UPDATE ON collection_items
FOR EACH ROW
EXECUTE FUNCTION update_content_timestamp();

View File

@@ -9,6 +9,80 @@ import (
"context" "context"
) )
const createCollectionItemVersionsLookupIndex = `-- name: CreateCollectionItemVersionsLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_item_versions_lookup ON collection_item_versions(item_id, collection_id, site_id, created_at DESC)
`
func (q *Queries) CreateCollectionItemVersionsLookupIndex(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, createCollectionItemVersionsLookupIndex)
return err
}
const createCollectionItemsLookupIndex = `-- name: CreateCollectionItemsLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_items_lookup ON collection_items(collection_id, site_id, position)
`
func (q *Queries) CreateCollectionItemsLookupIndex(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, createCollectionItemsLookupIndex)
return err
}
const createCollectionItemsTemplateIndex = `-- name: CreateCollectionItemsTemplateIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_items_template ON collection_items(template_id)
`
func (q *Queries) CreateCollectionItemsTemplateIndex(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, createCollectionItemsTemplateIndex)
return err
}
const createCollectionTemplatesDefaultIndex = `-- name: CreateCollectionTemplatesDefaultIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_templates_default ON collection_templates(collection_id, site_id, is_default DESC)
`
func (q *Queries) CreateCollectionTemplatesDefaultIndex(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, createCollectionTemplatesDefaultIndex)
return err
}
const createCollectionTemplatesLookupIndex = `-- name: CreateCollectionTemplatesLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_templates_lookup ON collection_templates(collection_id, site_id)
`
func (q *Queries) CreateCollectionTemplatesLookupIndex(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, createCollectionTemplatesLookupIndex)
return err
}
const createCollectionTemplatesOneDefaultIndex = `-- name: CreateCollectionTemplatesOneDefaultIndex :exec
CREATE UNIQUE INDEX IF NOT EXISTS idx_collection_templates_one_default
ON collection_templates(collection_id, site_id)
WHERE is_default = TRUE
`
func (q *Queries) CreateCollectionTemplatesOneDefaultIndex(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, createCollectionTemplatesOneDefaultIndex)
return err
}
const createCollectionsSiteIndex = `-- name: CreateCollectionsSiteIndex :exec
CREATE INDEX IF NOT EXISTS idx_collections_site_id ON collections(site_id)
`
func (q *Queries) CreateCollectionsSiteIndex(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, createCollectionsSiteIndex)
return err
}
const createCollectionsUpdatedAtIndex = `-- name: CreateCollectionsUpdatedAtIndex :exec
CREATE INDEX IF NOT EXISTS idx_collections_updated_at ON collections(updated_at)
`
func (q *Queries) CreateCollectionsUpdatedAtIndex(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, createCollectionsUpdatedAtIndex)
return err
}
const createContentSiteIndex = `-- name: CreateContentSiteIndex :exec const createContentSiteIndex = `-- name: CreateContentSiteIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id) CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id)
` `
@@ -51,6 +125,83 @@ func (q *Queries) CreateVersionsLookupIndex(ctx context.Context) error {
return err return err
} }
const initializeCollectionItemVersionsTable = `-- name: InitializeCollectionItemVersionsTable :exec
CREATE TABLE IF NOT EXISTS collection_item_versions (
version_id SERIAL PRIMARY KEY,
item_id TEXT NOT NULL,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
html_content TEXT NOT NULL,
template_id INTEGER NOT NULL,
position INTEGER NOT NULL,
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL,
FOREIGN KEY (item_id, collection_id, site_id) REFERENCES collection_items(item_id, collection_id, site_id) ON DELETE CASCADE
)
`
func (q *Queries) InitializeCollectionItemVersionsTable(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, initializeCollectionItemVersionsTable)
return err
}
const initializeCollectionItemsTable = `-- name: InitializeCollectionItemsTable :exec
CREATE TABLE IF NOT EXISTS collection_items (
item_id TEXT NOT NULL,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
template_id INTEGER NOT NULL,
html_content TEXT NOT NULL,
position INTEGER NOT NULL,
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
updated_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (item_id, collection_id, site_id),
FOREIGN KEY (collection_id, site_id) REFERENCES collections(id, site_id) ON DELETE CASCADE,
FOREIGN KEY (template_id) REFERENCES collection_templates(template_id) ON DELETE RESTRICT
)
`
func (q *Queries) InitializeCollectionItemsTable(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, initializeCollectionItemsTable)
return err
}
const initializeCollectionTemplatesTable = `-- name: InitializeCollectionTemplatesTable :exec
CREATE TABLE IF NOT EXISTS collection_templates (
template_id SERIAL PRIMARY KEY,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
name TEXT NOT NULL,
html_template TEXT NOT NULL,
is_default BOOLEAN DEFAULT FALSE NOT NULL,
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
FOREIGN KEY (collection_id, site_id) REFERENCES collections(id, site_id) ON DELETE CASCADE
)
`
func (q *Queries) InitializeCollectionTemplatesTable(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, initializeCollectionTemplatesTable)
return err
}
const initializeCollectionsTable = `-- name: InitializeCollectionsTable :exec
CREATE TABLE IF NOT EXISTS collections (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
container_html TEXT NOT NULL,
created_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
updated_at BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW())) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
)
`
func (q *Queries) InitializeCollectionsTable(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, initializeCollectionsTable)
return err
}
const initializeSchema = `-- name: InitializeSchema :exec const initializeSchema = `-- name: InitializeSchema :exec
CREATE TABLE IF NOT EXISTS content ( CREATE TABLE IF NOT EXISTS content (
id TEXT NOT NULL, id TEXT NOT NULL,

View File

@@ -0,0 +1,31 @@
-- Collection item versions table queries
-- name: CreateCollectionItemVersion :exec
INSERT INTO collection_item_versions (item_id, collection_id, site_id, html_content, template_id, position, created_by)
VALUES (sqlc.arg(item_id), sqlc.arg(collection_id), sqlc.arg(site_id), sqlc.arg(html_content), sqlc.arg(template_id), sqlc.arg(position), sqlc.arg(created_by));
-- name: GetCollectionItemVersionHistory :many
SELECT version_id, item_id, collection_id, site_id, html_content, template_id, position, created_at, created_by
FROM collection_item_versions
WHERE item_id = sqlc.arg(item_id) AND collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id)
ORDER BY created_at DESC
LIMIT sqlc.arg(limit_count);
-- name: GetCollectionItemVersion :one
SELECT version_id, item_id, collection_id, site_id, html_content, template_id, position, created_at, created_by
FROM collection_item_versions
WHERE version_id = sqlc.arg(version_id);
-- name: GetAllCollectionItemVersionsForSite :many
SELECT
civ.version_id, civ.item_id, civ.collection_id, civ.site_id, civ.html_content, civ.template_id, civ.position, civ.created_at, civ.created_by,
ci.html_content as current_html_content, ci.position as current_position
FROM collection_item_versions civ
LEFT JOIN collection_items ci ON civ.item_id = ci.item_id AND civ.collection_id = ci.collection_id AND civ.site_id = ci.site_id
WHERE civ.site_id = sqlc.arg(site_id)
ORDER BY civ.created_at DESC
LIMIT sqlc.arg(limit_count);
-- name: DeleteOldCollectionItemVersions :exec
DELETE FROM collection_item_versions
WHERE created_at < sqlc.arg(created_before) AND site_id = sqlc.arg(site_id);

View File

@@ -0,0 +1,57 @@
-- Collection items table queries
-- name: GetCollectionItem :one
SELECT item_id, collection_id, site_id, template_id, html_content, position, created_at, updated_at, last_edited_by
FROM collection_items
WHERE item_id = sqlc.arg(item_id) AND collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id);
-- name: GetCollectionItems :many
SELECT item_id, collection_id, site_id, template_id, html_content, position, created_at, updated_at, last_edited_by
FROM collection_items
WHERE collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id)
ORDER BY position ASC;
-- name: GetCollectionItemsWithTemplate :many
SELECT
ci.item_id, ci.collection_id, ci.site_id, ci.template_id, ci.html_content, ci.position, ci.created_at, ci.updated_at, ci.last_edited_by,
ct.name as template_name, ct.html_template, ct.is_default
FROM collection_items ci
JOIN collection_templates ct ON ci.template_id = ct.template_id
WHERE ci.collection_id = sqlc.arg(collection_id) AND ci.site_id = sqlc.arg(site_id)
ORDER BY ci.position ASC;
-- name: GetMaxPosition :one
SELECT COALESCE(MAX(position), 0) as max_position
FROM collection_items
WHERE collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id);
-- name: CreateCollectionItem :one
INSERT INTO collection_items (item_id, collection_id, site_id, template_id, html_content, position, last_edited_by)
VALUES (sqlc.arg(item_id), sqlc.arg(collection_id), sqlc.arg(site_id), sqlc.arg(template_id), sqlc.arg(html_content), sqlc.arg(position), sqlc.arg(last_edited_by))
RETURNING item_id, collection_id, site_id, template_id, html_content, position, created_at, updated_at, last_edited_by;
-- name: UpdateCollectionItem :one
UPDATE collection_items
SET html_content = sqlc.arg(html_content), last_edited_by = sqlc.arg(last_edited_by)
WHERE item_id = sqlc.arg(item_id) AND collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id)
RETURNING item_id, collection_id, site_id, template_id, html_content, position, created_at, updated_at, last_edited_by;
-- name: UpdateCollectionItemPosition :exec
UPDATE collection_items
SET position = sqlc.arg(position)
WHERE item_id = sqlc.arg(item_id) AND collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id);
-- name: ReorderCollectionItems :exec
UPDATE collection_items
SET position = position + sqlc.arg(position_delta)
WHERE collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id)
AND position >= sqlc.arg(start_position);
-- name: DeleteCollectionItem :exec
DELETE FROM collection_items
WHERE item_id = sqlc.arg(item_id) AND collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id);
-- name: DeleteCollectionItems :exec
DELETE FROM collection_items
WHERE collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id);

View File

@@ -0,0 +1,45 @@
-- Collection templates table queries
-- name: GetCollectionTemplate :one
SELECT template_id, collection_id, site_id, name, html_template, is_default, created_at
FROM collection_templates
WHERE template_id = sqlc.arg(template_id);
-- name: GetCollectionTemplates :many
SELECT template_id, collection_id, site_id, name, html_template, is_default, created_at
FROM collection_templates
WHERE collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id)
ORDER BY is_default DESC, created_at ASC;
-- name: GetDefaultTemplate :one
SELECT template_id, collection_id, site_id, name, html_template, is_default, created_at
FROM collection_templates
WHERE collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id) AND is_default = TRUE
LIMIT 1;
-- name: CreateCollectionTemplate :one
INSERT INTO collection_templates (collection_id, site_id, name, html_template, is_default)
VALUES (sqlc.arg(collection_id), sqlc.arg(site_id), sqlc.arg(name), sqlc.arg(html_template), sqlc.arg(is_default))
RETURNING template_id, collection_id, site_id, name, html_template, is_default, created_at;
-- name: UpdateCollectionTemplate :one
UPDATE collection_templates
SET name = sqlc.arg(name), html_template = sqlc.arg(html_template), is_default = sqlc.arg(is_default)
WHERE template_id = sqlc.arg(template_id)
RETURNING template_id, collection_id, site_id, name, html_template, is_default, created_at;
-- name: SetTemplateAsDefault :exec
UPDATE collection_templates
SET is_default = CASE
WHEN template_id = sqlc.arg(template_id) THEN TRUE
ELSE FALSE
END
WHERE collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id);
-- name: DeleteCollectionTemplate :exec
DELETE FROM collection_templates
WHERE template_id = sqlc.arg(template_id);
-- name: DeleteCollectionTemplates :exec
DELETE FROM collection_templates
WHERE collection_id = sqlc.arg(collection_id) AND site_id = sqlc.arg(site_id);

View File

@@ -0,0 +1,39 @@
-- Collections table queries
-- name: GetCollection :one
SELECT id, site_id, container_html, created_at, updated_at, last_edited_by
FROM collections
WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id);
-- name: GetAllCollections :many
SELECT id, site_id, container_html, created_at, updated_at, last_edited_by
FROM collections
WHERE site_id = sqlc.arg(site_id)
ORDER BY updated_at DESC;
-- name: CreateCollection :one
INSERT INTO collections (id, site_id, container_html, last_edited_by)
VALUES (sqlc.arg(id), sqlc.arg(site_id), sqlc.arg(container_html), sqlc.arg(last_edited_by))
RETURNING id, site_id, container_html, created_at, updated_at, last_edited_by;
-- name: UpdateCollection :one
UPDATE collections
SET container_html = sqlc.arg(container_html), last_edited_by = sqlc.arg(last_edited_by)
WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id)
RETURNING id, site_id, container_html, created_at, updated_at, last_edited_by;
-- name: UpsertCollection :one
INSERT INTO collections (id, site_id, container_html, last_edited_by)
VALUES (sqlc.arg(id), sqlc.arg(site_id), sqlc.arg(container_html), sqlc.arg(last_edited_by))
ON CONFLICT(id, site_id) DO UPDATE SET
container_html = EXCLUDED.container_html,
last_edited_by = EXCLUDED.last_edited_by
RETURNING id, site_id, container_html, created_at, updated_at, last_edited_by;
-- name: DeleteCollection :exec
DELETE FROM collections
WHERE id = sqlc.arg(id) AND site_id = sqlc.arg(site_id);
-- name: DeleteAllSiteCollections :exec
DELETE FROM collections
WHERE site_id = sqlc.arg(site_id);

View File

@@ -0,0 +1,197 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: collection_item_versions.sql
package sqlite
import (
"context"
"database/sql"
)
const createCollectionItemVersion = `-- name: CreateCollectionItemVersion :exec
INSERT INTO collection_item_versions (item_id, collection_id, site_id, html_content, template_id, position, created_by)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
`
type CreateCollectionItemVersionParams struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
TemplateID int64 `json:"template_id"`
Position int64 `json:"position"`
CreatedBy string `json:"created_by"`
}
// Collection item versions table queries
func (q *Queries) CreateCollectionItemVersion(ctx context.Context, arg CreateCollectionItemVersionParams) error {
_, err := q.db.ExecContext(ctx, createCollectionItemVersion,
arg.ItemID,
arg.CollectionID,
arg.SiteID,
arg.HtmlContent,
arg.TemplateID,
arg.Position,
arg.CreatedBy,
)
return err
}
const deleteOldCollectionItemVersions = `-- name: DeleteOldCollectionItemVersions :exec
DELETE FROM collection_item_versions
WHERE created_at < ?1 AND site_id = ?2
`
type DeleteOldCollectionItemVersionsParams struct {
CreatedBefore int64 `json:"created_before"`
SiteID string `json:"site_id"`
}
func (q *Queries) DeleteOldCollectionItemVersions(ctx context.Context, arg DeleteOldCollectionItemVersionsParams) error {
_, err := q.db.ExecContext(ctx, deleteOldCollectionItemVersions, arg.CreatedBefore, arg.SiteID)
return err
}
const getAllCollectionItemVersionsForSite = `-- name: GetAllCollectionItemVersionsForSite :many
SELECT
civ.version_id, civ.item_id, civ.collection_id, civ.site_id, civ.html_content, civ.template_id, civ.position, civ.created_at, civ.created_by,
ci.html_content as current_html_content, ci.position as current_position
FROM collection_item_versions civ
LEFT JOIN collection_items ci ON civ.item_id = ci.item_id AND civ.collection_id = ci.collection_id AND civ.site_id = ci.site_id
WHERE civ.site_id = ?1
ORDER BY civ.created_at DESC
LIMIT ?2
`
type GetAllCollectionItemVersionsForSiteParams struct {
SiteID string `json:"site_id"`
LimitCount int64 `json:"limit_count"`
}
type GetAllCollectionItemVersionsForSiteRow struct {
VersionID int64 `json:"version_id"`
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
TemplateID int64 `json:"template_id"`
Position int64 `json:"position"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
CurrentHtmlContent sql.NullString `json:"current_html_content"`
CurrentPosition sql.NullInt64 `json:"current_position"`
}
func (q *Queries) GetAllCollectionItemVersionsForSite(ctx context.Context, arg GetAllCollectionItemVersionsForSiteParams) ([]GetAllCollectionItemVersionsForSiteRow, error) {
rows, err := q.db.QueryContext(ctx, getAllCollectionItemVersionsForSite, arg.SiteID, arg.LimitCount)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllCollectionItemVersionsForSiteRow
for rows.Next() {
var i GetAllCollectionItemVersionsForSiteRow
if err := rows.Scan(
&i.VersionID,
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.HtmlContent,
&i.TemplateID,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
&i.CurrentHtmlContent,
&i.CurrentPosition,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getCollectionItemVersion = `-- name: GetCollectionItemVersion :one
SELECT version_id, item_id, collection_id, site_id, html_content, template_id, position, created_at, created_by
FROM collection_item_versions
WHERE version_id = ?1
`
func (q *Queries) GetCollectionItemVersion(ctx context.Context, versionID int64) (CollectionItemVersion, error) {
row := q.db.QueryRowContext(ctx, getCollectionItemVersion, versionID)
var i CollectionItemVersion
err := row.Scan(
&i.VersionID,
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.HtmlContent,
&i.TemplateID,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
)
return i, err
}
const getCollectionItemVersionHistory = `-- name: GetCollectionItemVersionHistory :many
SELECT version_id, item_id, collection_id, site_id, html_content, template_id, position, created_at, created_by
FROM collection_item_versions
WHERE item_id = ?1 AND collection_id = ?2 AND site_id = ?3
ORDER BY created_at DESC
LIMIT ?4
`
type GetCollectionItemVersionHistoryParams struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
LimitCount int64 `json:"limit_count"`
}
func (q *Queries) GetCollectionItemVersionHistory(ctx context.Context, arg GetCollectionItemVersionHistoryParams) ([]CollectionItemVersion, error) {
rows, err := q.db.QueryContext(ctx, getCollectionItemVersionHistory,
arg.ItemID,
arg.CollectionID,
arg.SiteID,
arg.LimitCount,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CollectionItemVersion
for rows.Next() {
var i CollectionItemVersion
if err := rows.Scan(
&i.VersionID,
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.HtmlContent,
&i.TemplateID,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@@ -0,0 +1,327 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: collection_items.sql
package sqlite
import (
"context"
)
const createCollectionItem = `-- name: CreateCollectionItem :one
INSERT INTO collection_items (item_id, collection_id, site_id, template_id, html_content, position, last_edited_by)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
RETURNING item_id, collection_id, site_id, template_id, html_content, position, created_at, updated_at, last_edited_by
`
type CreateCollectionItemParams struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
TemplateID int64 `json:"template_id"`
HtmlContent string `json:"html_content"`
Position int64 `json:"position"`
LastEditedBy string `json:"last_edited_by"`
}
func (q *Queries) CreateCollectionItem(ctx context.Context, arg CreateCollectionItemParams) (CollectionItem, error) {
row := q.db.QueryRowContext(ctx, createCollectionItem,
arg.ItemID,
arg.CollectionID,
arg.SiteID,
arg.TemplateID,
arg.HtmlContent,
arg.Position,
arg.LastEditedBy,
)
var i CollectionItem
err := row.Scan(
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.TemplateID,
&i.HtmlContent,
&i.Position,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const deleteCollectionItem = `-- name: DeleteCollectionItem :exec
DELETE FROM collection_items
WHERE item_id = ?1 AND collection_id = ?2 AND site_id = ?3
`
type DeleteCollectionItemParams struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) DeleteCollectionItem(ctx context.Context, arg DeleteCollectionItemParams) error {
_, err := q.db.ExecContext(ctx, deleteCollectionItem, arg.ItemID, arg.CollectionID, arg.SiteID)
return err
}
const deleteCollectionItems = `-- name: DeleteCollectionItems :exec
DELETE FROM collection_items
WHERE collection_id = ?1 AND site_id = ?2
`
type DeleteCollectionItemsParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) DeleteCollectionItems(ctx context.Context, arg DeleteCollectionItemsParams) error {
_, err := q.db.ExecContext(ctx, deleteCollectionItems, arg.CollectionID, arg.SiteID)
return err
}
const getCollectionItem = `-- name: GetCollectionItem :one
SELECT item_id, collection_id, site_id, template_id, html_content, position, created_at, updated_at, last_edited_by
FROM collection_items
WHERE item_id = ?1 AND collection_id = ?2 AND site_id = ?3
`
type GetCollectionItemParams struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
// Collection items table queries
func (q *Queries) GetCollectionItem(ctx context.Context, arg GetCollectionItemParams) (CollectionItem, error) {
row := q.db.QueryRowContext(ctx, getCollectionItem, arg.ItemID, arg.CollectionID, arg.SiteID)
var i CollectionItem
err := row.Scan(
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.TemplateID,
&i.HtmlContent,
&i.Position,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const getCollectionItems = `-- name: GetCollectionItems :many
SELECT item_id, collection_id, site_id, template_id, html_content, position, created_at, updated_at, last_edited_by
FROM collection_items
WHERE collection_id = ?1 AND site_id = ?2
ORDER BY position ASC
`
type GetCollectionItemsParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) GetCollectionItems(ctx context.Context, arg GetCollectionItemsParams) ([]CollectionItem, error) {
rows, err := q.db.QueryContext(ctx, getCollectionItems, arg.CollectionID, arg.SiteID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CollectionItem
for rows.Next() {
var i CollectionItem
if err := rows.Scan(
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.TemplateID,
&i.HtmlContent,
&i.Position,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getCollectionItemsWithTemplate = `-- name: GetCollectionItemsWithTemplate :many
SELECT
ci.item_id, ci.collection_id, ci.site_id, ci.template_id, ci.html_content, ci.position, ci.created_at, ci.updated_at, ci.last_edited_by,
ct.name as template_name, ct.html_template, ct.is_default
FROM collection_items ci
JOIN collection_templates ct ON ci.template_id = ct.template_id
WHERE ci.collection_id = ?1 AND ci.site_id = ?2
ORDER BY ci.position ASC
`
type GetCollectionItemsWithTemplateParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
type GetCollectionItemsWithTemplateRow struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
TemplateID int64 `json:"template_id"`
HtmlContent string `json:"html_content"`
Position int64 `json:"position"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
TemplateName string `json:"template_name"`
HtmlTemplate string `json:"html_template"`
IsDefault int64 `json:"is_default"`
}
func (q *Queries) GetCollectionItemsWithTemplate(ctx context.Context, arg GetCollectionItemsWithTemplateParams) ([]GetCollectionItemsWithTemplateRow, error) {
rows, err := q.db.QueryContext(ctx, getCollectionItemsWithTemplate, arg.CollectionID, arg.SiteID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetCollectionItemsWithTemplateRow
for rows.Next() {
var i GetCollectionItemsWithTemplateRow
if err := rows.Scan(
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.TemplateID,
&i.HtmlContent,
&i.Position,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
&i.TemplateName,
&i.HtmlTemplate,
&i.IsDefault,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getMaxPosition = `-- name: GetMaxPosition :one
SELECT COALESCE(MAX(position), 0) as max_position
FROM collection_items
WHERE collection_id = ?1 AND site_id = ?2
`
type GetMaxPositionParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) GetMaxPosition(ctx context.Context, arg GetMaxPositionParams) (interface{}, error) {
row := q.db.QueryRowContext(ctx, getMaxPosition, arg.CollectionID, arg.SiteID)
var max_position interface{}
err := row.Scan(&max_position)
return max_position, err
}
const reorderCollectionItems = `-- name: ReorderCollectionItems :exec
UPDATE collection_items
SET position = position + ?1
WHERE collection_id = ?2 AND site_id = ?3
AND position >= ?4
`
type ReorderCollectionItemsParams struct {
PositionDelta int64 `json:"position_delta"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
StartPosition int64 `json:"start_position"`
}
func (q *Queries) ReorderCollectionItems(ctx context.Context, arg ReorderCollectionItemsParams) error {
_, err := q.db.ExecContext(ctx, reorderCollectionItems,
arg.PositionDelta,
arg.CollectionID,
arg.SiteID,
arg.StartPosition,
)
return err
}
const updateCollectionItem = `-- name: UpdateCollectionItem :one
UPDATE collection_items
SET html_content = ?1, last_edited_by = ?2
WHERE item_id = ?3 AND collection_id = ?4 AND site_id = ?5
RETURNING item_id, collection_id, site_id, template_id, html_content, position, created_at, updated_at, last_edited_by
`
type UpdateCollectionItemParams struct {
HtmlContent string `json:"html_content"`
LastEditedBy string `json:"last_edited_by"`
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) UpdateCollectionItem(ctx context.Context, arg UpdateCollectionItemParams) (CollectionItem, error) {
row := q.db.QueryRowContext(ctx, updateCollectionItem,
arg.HtmlContent,
arg.LastEditedBy,
arg.ItemID,
arg.CollectionID,
arg.SiteID,
)
var i CollectionItem
err := row.Scan(
&i.ItemID,
&i.CollectionID,
&i.SiteID,
&i.TemplateID,
&i.HtmlContent,
&i.Position,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const updateCollectionItemPosition = `-- name: UpdateCollectionItemPosition :exec
UPDATE collection_items
SET position = ?1
WHERE item_id = ?2 AND collection_id = ?3 AND site_id = ?4
`
type UpdateCollectionItemPositionParams struct {
Position int64 `json:"position"`
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) UpdateCollectionItemPosition(ctx context.Context, arg UpdateCollectionItemPositionParams) error {
_, err := q.db.ExecContext(ctx, updateCollectionItemPosition,
arg.Position,
arg.ItemID,
arg.CollectionID,
arg.SiteID,
)
return err
}

View File

@@ -0,0 +1,217 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: collection_templates.sql
package sqlite
import (
"context"
)
const createCollectionTemplate = `-- name: CreateCollectionTemplate :one
INSERT INTO collection_templates (collection_id, site_id, name, html_template, is_default)
VALUES (?1, ?2, ?3, ?4, ?5)
RETURNING template_id, collection_id, site_id, name, html_template, is_default, created_at
`
type CreateCollectionTemplateParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
Name string `json:"name"`
HtmlTemplate string `json:"html_template"`
IsDefault int64 `json:"is_default"`
}
func (q *Queries) CreateCollectionTemplate(ctx context.Context, arg CreateCollectionTemplateParams) (CollectionTemplate, error) {
row := q.db.QueryRowContext(ctx, createCollectionTemplate,
arg.CollectionID,
arg.SiteID,
arg.Name,
arg.HtmlTemplate,
arg.IsDefault,
)
var i CollectionTemplate
err := row.Scan(
&i.TemplateID,
&i.CollectionID,
&i.SiteID,
&i.Name,
&i.HtmlTemplate,
&i.IsDefault,
&i.CreatedAt,
)
return i, err
}
const deleteCollectionTemplate = `-- name: DeleteCollectionTemplate :exec
DELETE FROM collection_templates
WHERE template_id = ?1
`
func (q *Queries) DeleteCollectionTemplate(ctx context.Context, templateID int64) error {
_, err := q.db.ExecContext(ctx, deleteCollectionTemplate, templateID)
return err
}
const deleteCollectionTemplates = `-- name: DeleteCollectionTemplates :exec
DELETE FROM collection_templates
WHERE collection_id = ?1 AND site_id = ?2
`
type DeleteCollectionTemplatesParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) DeleteCollectionTemplates(ctx context.Context, arg DeleteCollectionTemplatesParams) error {
_, err := q.db.ExecContext(ctx, deleteCollectionTemplates, arg.CollectionID, arg.SiteID)
return err
}
const getCollectionTemplate = `-- name: GetCollectionTemplate :one
SELECT template_id, collection_id, site_id, name, html_template, is_default, created_at
FROM collection_templates
WHERE template_id = ?1
`
// Collection templates table queries
func (q *Queries) GetCollectionTemplate(ctx context.Context, templateID int64) (CollectionTemplate, error) {
row := q.db.QueryRowContext(ctx, getCollectionTemplate, templateID)
var i CollectionTemplate
err := row.Scan(
&i.TemplateID,
&i.CollectionID,
&i.SiteID,
&i.Name,
&i.HtmlTemplate,
&i.IsDefault,
&i.CreatedAt,
)
return i, err
}
const getCollectionTemplates = `-- name: GetCollectionTemplates :many
SELECT template_id, collection_id, site_id, name, html_template, is_default, created_at
FROM collection_templates
WHERE collection_id = ?1 AND site_id = ?2
ORDER BY is_default DESC, created_at ASC
`
type GetCollectionTemplatesParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) GetCollectionTemplates(ctx context.Context, arg GetCollectionTemplatesParams) ([]CollectionTemplate, error) {
rows, err := q.db.QueryContext(ctx, getCollectionTemplates, arg.CollectionID, arg.SiteID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CollectionTemplate
for rows.Next() {
var i CollectionTemplate
if err := rows.Scan(
&i.TemplateID,
&i.CollectionID,
&i.SiteID,
&i.Name,
&i.HtmlTemplate,
&i.IsDefault,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getDefaultTemplate = `-- name: GetDefaultTemplate :one
SELECT template_id, collection_id, site_id, name, html_template, is_default, created_at
FROM collection_templates
WHERE collection_id = ?1 AND site_id = ?2 AND is_default = TRUE
LIMIT 1
`
type GetDefaultTemplateParams struct {
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) GetDefaultTemplate(ctx context.Context, arg GetDefaultTemplateParams) (CollectionTemplate, error) {
row := q.db.QueryRowContext(ctx, getDefaultTemplate, arg.CollectionID, arg.SiteID)
var i CollectionTemplate
err := row.Scan(
&i.TemplateID,
&i.CollectionID,
&i.SiteID,
&i.Name,
&i.HtmlTemplate,
&i.IsDefault,
&i.CreatedAt,
)
return i, err
}
const setTemplateAsDefault = `-- name: SetTemplateAsDefault :exec
UPDATE collection_templates
SET is_default = CASE
WHEN template_id = ?1 THEN TRUE
ELSE FALSE
END
WHERE collection_id = ?2 AND site_id = ?3
`
type SetTemplateAsDefaultParams struct {
TemplateID int64 `json:"template_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
}
func (q *Queries) SetTemplateAsDefault(ctx context.Context, arg SetTemplateAsDefaultParams) error {
_, err := q.db.ExecContext(ctx, setTemplateAsDefault, arg.TemplateID, arg.CollectionID, arg.SiteID)
return err
}
const updateCollectionTemplate = `-- name: UpdateCollectionTemplate :one
UPDATE collection_templates
SET name = ?1, html_template = ?2, is_default = ?3
WHERE template_id = ?4
RETURNING template_id, collection_id, site_id, name, html_template, is_default, created_at
`
type UpdateCollectionTemplateParams struct {
Name string `json:"name"`
HtmlTemplate string `json:"html_template"`
IsDefault int64 `json:"is_default"`
TemplateID int64 `json:"template_id"`
}
func (q *Queries) UpdateCollectionTemplate(ctx context.Context, arg UpdateCollectionTemplateParams) (CollectionTemplate, error) {
row := q.db.QueryRowContext(ctx, updateCollectionTemplate,
arg.Name,
arg.HtmlTemplate,
arg.IsDefault,
arg.TemplateID,
)
var i CollectionTemplate
err := row.Scan(
&i.TemplateID,
&i.CollectionID,
&i.SiteID,
&i.Name,
&i.HtmlTemplate,
&i.IsDefault,
&i.CreatedAt,
)
return i, err
}

View File

@@ -0,0 +1,199 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: collections.sql
package sqlite
import (
"context"
)
const createCollection = `-- name: CreateCollection :one
INSERT INTO collections (id, site_id, container_html, last_edited_by)
VALUES (?1, ?2, ?3, ?4)
RETURNING id, site_id, container_html, created_at, updated_at, last_edited_by
`
type CreateCollectionParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
ContainerHtml string `json:"container_html"`
LastEditedBy string `json:"last_edited_by"`
}
func (q *Queries) CreateCollection(ctx context.Context, arg CreateCollectionParams) (Collection, error) {
row := q.db.QueryRowContext(ctx, createCollection,
arg.ID,
arg.SiteID,
arg.ContainerHtml,
arg.LastEditedBy,
)
var i Collection
err := row.Scan(
&i.ID,
&i.SiteID,
&i.ContainerHtml,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const deleteAllSiteCollections = `-- name: DeleteAllSiteCollections :exec
DELETE FROM collections
WHERE site_id = ?1
`
func (q *Queries) DeleteAllSiteCollections(ctx context.Context, siteID string) error {
_, err := q.db.ExecContext(ctx, deleteAllSiteCollections, siteID)
return err
}
const deleteCollection = `-- name: DeleteCollection :exec
DELETE FROM collections
WHERE id = ?1 AND site_id = ?2
`
type DeleteCollectionParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
}
func (q *Queries) DeleteCollection(ctx context.Context, arg DeleteCollectionParams) error {
_, err := q.db.ExecContext(ctx, deleteCollection, arg.ID, arg.SiteID)
return err
}
const getAllCollections = `-- name: GetAllCollections :many
SELECT id, site_id, container_html, created_at, updated_at, last_edited_by
FROM collections
WHERE site_id = ?1
ORDER BY updated_at DESC
`
func (q *Queries) GetAllCollections(ctx context.Context, siteID string) ([]Collection, error) {
rows, err := q.db.QueryContext(ctx, getAllCollections, siteID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Collection
for rows.Next() {
var i Collection
if err := rows.Scan(
&i.ID,
&i.SiteID,
&i.ContainerHtml,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getCollection = `-- name: GetCollection :one
SELECT id, site_id, container_html, created_at, updated_at, last_edited_by
FROM collections
WHERE id = ?1 AND site_id = ?2
`
type GetCollectionParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
}
// Collections table queries
func (q *Queries) GetCollection(ctx context.Context, arg GetCollectionParams) (Collection, error) {
row := q.db.QueryRowContext(ctx, getCollection, arg.ID, arg.SiteID)
var i Collection
err := row.Scan(
&i.ID,
&i.SiteID,
&i.ContainerHtml,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const updateCollection = `-- name: UpdateCollection :one
UPDATE collections
SET container_html = ?1, last_edited_by = ?2
WHERE id = ?3 AND site_id = ?4
RETURNING id, site_id, container_html, created_at, updated_at, last_edited_by
`
type UpdateCollectionParams struct {
ContainerHtml string `json:"container_html"`
LastEditedBy string `json:"last_edited_by"`
ID string `json:"id"`
SiteID string `json:"site_id"`
}
func (q *Queries) UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (Collection, error) {
row := q.db.QueryRowContext(ctx, updateCollection,
arg.ContainerHtml,
arg.LastEditedBy,
arg.ID,
arg.SiteID,
)
var i Collection
err := row.Scan(
&i.ID,
&i.SiteID,
&i.ContainerHtml,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}
const upsertCollection = `-- name: UpsertCollection :one
INSERT INTO collections (id, site_id, container_html, last_edited_by)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(id, site_id) DO UPDATE SET
container_html = EXCLUDED.container_html,
last_edited_by = EXCLUDED.last_edited_by
RETURNING id, site_id, container_html, created_at, updated_at, last_edited_by
`
type UpsertCollectionParams struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
ContainerHtml string `json:"container_html"`
LastEditedBy string `json:"last_edited_by"`
}
func (q *Queries) UpsertCollection(ctx context.Context, arg UpsertCollectionParams) (Collection, error) {
row := q.db.QueryRowContext(ctx, upsertCollection,
arg.ID,
arg.SiteID,
arg.ContainerHtml,
arg.LastEditedBy,
)
var i Collection
err := row.Scan(
&i.ID,
&i.SiteID,
&i.ContainerHtml,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastEditedBy,
)
return i, err
}

View File

@@ -8,6 +8,49 @@ import (
"database/sql" "database/sql"
) )
type Collection struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
ContainerHtml string `json:"container_html"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
}
type CollectionItem struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
TemplateID int64 `json:"template_id"`
HtmlContent string `json:"html_content"`
Position int64 `json:"position"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
}
type CollectionItemVersion struct {
VersionID int64 `json:"version_id"`
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
HtmlContent string `json:"html_content"`
TemplateID int64 `json:"template_id"`
Position int64 `json:"position"`
CreatedAt int64 `json:"created_at"`
CreatedBy string `json:"created_by"`
}
type CollectionTemplate struct {
TemplateID int64 `json:"template_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
Name string `json:"name"`
HtmlTemplate string `json:"html_template"`
IsDefault int64 `json:"is_default"`
CreatedAt int64 `json:"created_at"`
}
type Content struct { type Content struct {
ID string `json:"id"` ID string `json:"id"`
SiteID string `json:"site_id"` SiteID string `json:"site_id"`

View File

@@ -9,20 +9,58 @@ import (
) )
type Querier interface { type Querier interface {
CreateCollection(ctx context.Context, arg CreateCollectionParams) (Collection, error)
CreateCollectionItem(ctx context.Context, arg CreateCollectionItemParams) (CollectionItem, error)
// Collection item versions table queries
CreateCollectionItemVersion(ctx context.Context, arg CreateCollectionItemVersionParams) error
CreateCollectionTemplate(ctx context.Context, arg CreateCollectionTemplateParams) (CollectionTemplate, error)
CreateContent(ctx context.Context, arg CreateContentParams) (Content, error) CreateContent(ctx context.Context, arg CreateContentParams) (Content, error)
CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error
DeleteAllSiteCollections(ctx context.Context, siteID string) error
DeleteAllSiteContent(ctx context.Context, siteID string) error DeleteAllSiteContent(ctx context.Context, siteID string) error
DeleteCollection(ctx context.Context, arg DeleteCollectionParams) error
DeleteCollectionItem(ctx context.Context, arg DeleteCollectionItemParams) error
DeleteCollectionItems(ctx context.Context, arg DeleteCollectionItemsParams) error
DeleteCollectionTemplate(ctx context.Context, templateID int64) error
DeleteCollectionTemplates(ctx context.Context, arg DeleteCollectionTemplatesParams) error
DeleteContent(ctx context.Context, arg DeleteContentParams) error DeleteContent(ctx context.Context, arg DeleteContentParams) error
DeleteOldCollectionItemVersions(ctx context.Context, arg DeleteOldCollectionItemVersionsParams) error
DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsParams) error DeleteOldVersions(ctx context.Context, arg DeleteOldVersionsParams) error
GetAllCollectionItemVersionsForSite(ctx context.Context, arg GetAllCollectionItemVersionsForSiteParams) ([]GetAllCollectionItemVersionsForSiteRow, error)
GetAllCollections(ctx context.Context, siteID string) ([]Collection, error)
GetAllContent(ctx context.Context, siteID string) ([]Content, error) GetAllContent(ctx context.Context, siteID string) ([]Content, error)
GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsForSiteParams) ([]GetAllVersionsForSiteRow, error) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsForSiteParams) ([]GetAllVersionsForSiteRow, error)
GetBulkContent(ctx context.Context, arg GetBulkContentParams) ([]Content, error) GetBulkContent(ctx context.Context, arg GetBulkContentParams) ([]Content, error)
// Collections table queries
GetCollection(ctx context.Context, arg GetCollectionParams) (Collection, error)
// Collection items table queries
GetCollectionItem(ctx context.Context, arg GetCollectionItemParams) (CollectionItem, error)
GetCollectionItemVersion(ctx context.Context, versionID int64) (CollectionItemVersion, error)
GetCollectionItemVersionHistory(ctx context.Context, arg GetCollectionItemVersionHistoryParams) ([]CollectionItemVersion, error)
GetCollectionItems(ctx context.Context, arg GetCollectionItemsParams) ([]CollectionItem, error)
GetCollectionItemsWithTemplate(ctx context.Context, arg GetCollectionItemsWithTemplateParams) ([]GetCollectionItemsWithTemplateRow, error)
// Collection templates table queries
GetCollectionTemplate(ctx context.Context, templateID int64) (CollectionTemplate, error)
GetCollectionTemplates(ctx context.Context, arg GetCollectionTemplatesParams) ([]CollectionTemplate, error)
GetContent(ctx context.Context, arg GetContentParams) (Content, error) GetContent(ctx context.Context, arg GetContentParams) (Content, error)
GetContentVersion(ctx context.Context, versionID int64) (ContentVersion, error) GetContentVersion(ctx context.Context, versionID int64) (ContentVersion, error)
GetContentVersionHistory(ctx context.Context, arg GetContentVersionHistoryParams) ([]ContentVersion, error) GetContentVersionHistory(ctx context.Context, arg GetContentVersionHistoryParams) ([]ContentVersion, error)
GetDefaultTemplate(ctx context.Context, arg GetDefaultTemplateParams) (CollectionTemplate, error)
GetMaxPosition(ctx context.Context, arg GetMaxPositionParams) (interface{}, error)
InitializeCollectionItemVersionsTable(ctx context.Context) error
InitializeCollectionItemsTable(ctx context.Context) error
InitializeCollectionTemplatesTable(ctx context.Context) error
InitializeCollectionsTable(ctx context.Context) error
InitializeSchema(ctx context.Context) error InitializeSchema(ctx context.Context) error
InitializeVersionsTable(ctx context.Context) error InitializeVersionsTable(ctx context.Context) error
ReorderCollectionItems(ctx context.Context, arg ReorderCollectionItemsParams) error
SetTemplateAsDefault(ctx context.Context, arg SetTemplateAsDefaultParams) error
UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (Collection, error)
UpdateCollectionItem(ctx context.Context, arg UpdateCollectionItemParams) (CollectionItem, error)
UpdateCollectionItemPosition(ctx context.Context, arg UpdateCollectionItemPositionParams) error
UpdateCollectionTemplate(ctx context.Context, arg UpdateCollectionTemplateParams) (CollectionTemplate, error)
UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error) UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error)
UpsertCollection(ctx context.Context, arg UpsertCollectionParams) (Collection, error)
UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error)
} }

View File

@@ -22,11 +22,78 @@ CREATE TABLE content_versions (
created_by TEXT DEFAULT 'system' NOT NULL created_by TEXT DEFAULT 'system' NOT NULL
); );
-- Collections table - manages .insertr-add containers
CREATE TABLE collections (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
container_html TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
);
-- Collection templates - multiple template variants per collection
CREATE TABLE collection_templates (
template_id INTEGER PRIMARY KEY AUTOINCREMENT,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
name TEXT NOT NULL,
html_template TEXT NOT NULL,
is_default INTEGER DEFAULT 0 NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
FOREIGN KEY (collection_id, site_id) REFERENCES collections(id, site_id) ON DELETE CASCADE
);
-- Collection items - individual items within collections
CREATE TABLE collection_items (
item_id TEXT NOT NULL,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
template_id INTEGER NOT NULL,
html_content TEXT NOT NULL,
position INTEGER NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (item_id, collection_id, site_id),
FOREIGN KEY (collection_id, site_id) REFERENCES collections(id, site_id) ON DELETE CASCADE,
FOREIGN KEY (template_id) REFERENCES collection_templates(template_id) ON DELETE RESTRICT
);
-- Collection item version history
CREATE TABLE collection_item_versions (
version_id INTEGER PRIMARY KEY AUTOINCREMENT,
item_id TEXT NOT NULL,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
html_content TEXT NOT NULL,
template_id INTEGER NOT NULL,
position INTEGER NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL,
FOREIGN KEY (item_id, collection_id, site_id) REFERENCES collection_items(item_id, collection_id, site_id) ON DELETE CASCADE
);
-- Indexes for performance -- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id); CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id);
CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at); CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at);
CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC);
-- Collection indexes for performance
CREATE INDEX IF NOT EXISTS idx_collections_site_id ON collections(site_id);
CREATE INDEX IF NOT EXISTS idx_collections_updated_at ON collections(updated_at);
CREATE INDEX IF NOT EXISTS idx_collection_templates_lookup ON collection_templates(collection_id, site_id);
CREATE INDEX IF NOT EXISTS idx_collection_templates_default ON collection_templates(collection_id, site_id, is_default DESC);
CREATE INDEX IF NOT EXISTS idx_collection_items_lookup ON collection_items(collection_id, site_id, position);
CREATE INDEX IF NOT EXISTS idx_collection_items_template ON collection_items(template_id);
CREATE INDEX IF NOT EXISTS idx_collection_item_versions_lookup ON collection_item_versions(item_id, collection_id, site_id, created_at DESC);
-- Constraint to ensure only one default template per collection
CREATE UNIQUE INDEX IF NOT EXISTS idx_collection_templates_one_default
ON collection_templates(collection_id, site_id)
WHERE is_default = 1;
-- Trigger to automatically update updated_at timestamp -- Trigger to automatically update updated_at timestamp
CREATE TRIGGER IF NOT EXISTS update_content_updated_at CREATE TRIGGER IF NOT EXISTS update_content_updated_at
AFTER UPDATE ON content AFTER UPDATE ON content
@@ -34,3 +101,18 @@ FOR EACH ROW
BEGIN BEGIN
UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id; UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id;
END; END;
-- Triggers for collection timestamps
CREATE TRIGGER IF NOT EXISTS update_collections_updated_at
AFTER UPDATE ON collections
FOR EACH ROW
BEGIN
UPDATE collections SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id;
END;
CREATE TRIGGER IF NOT EXISTS update_collection_items_updated_at
AFTER UPDATE ON collection_items
FOR EACH ROW
BEGIN
UPDATE collection_items SET updated_at = strftime('%s', 'now') WHERE item_id = NEW.item_id AND collection_id = NEW.collection_id AND site_id = NEW.site_id;
END;

View File

@@ -21,6 +21,59 @@ CREATE TABLE IF NOT EXISTS content_versions (
created_by TEXT DEFAULT 'system' NOT NULL created_by TEXT DEFAULT 'system' NOT NULL
); );
-- name: InitializeCollectionsTable :exec
CREATE TABLE IF NOT EXISTS collections (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
container_html TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
);
-- name: InitializeCollectionTemplatesTable :exec
CREATE TABLE IF NOT EXISTS collection_templates (
template_id INTEGER PRIMARY KEY AUTOINCREMENT,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
name TEXT NOT NULL,
html_template TEXT NOT NULL,
is_default INTEGER DEFAULT 0 NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
FOREIGN KEY (collection_id, site_id) REFERENCES collections(id, site_id) ON DELETE CASCADE
);
-- name: InitializeCollectionItemsTable :exec
CREATE TABLE IF NOT EXISTS collection_items (
item_id TEXT NOT NULL,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
template_id INTEGER NOT NULL,
html_content TEXT NOT NULL,
position INTEGER NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (item_id, collection_id, site_id),
FOREIGN KEY (collection_id, site_id) REFERENCES collections(id, site_id) ON DELETE CASCADE,
FOREIGN KEY (template_id) REFERENCES collection_templates(template_id) ON DELETE RESTRICT
);
-- name: InitializeCollectionItemVersionsTable :exec
CREATE TABLE IF NOT EXISTS collection_item_versions (
version_id INTEGER PRIMARY KEY AUTOINCREMENT,
item_id TEXT NOT NULL,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
html_content TEXT NOT NULL,
template_id INTEGER NOT NULL,
position INTEGER NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL,
FOREIGN KEY (item_id, collection_id, site_id) REFERENCES collection_items(item_id, collection_id, site_id) ON DELETE CASCADE
);
-- name: CreateContentSiteIndex :exec -- name: CreateContentSiteIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id); CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id);
@@ -30,6 +83,32 @@ CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at);
-- name: CreateVersionsLookupIndex :exec -- name: CreateVersionsLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC);
-- name: CreateCollectionsSiteIndex :exec
CREATE INDEX IF NOT EXISTS idx_collections_site_id ON collections(site_id);
-- name: CreateCollectionsUpdatedAtIndex :exec
CREATE INDEX IF NOT EXISTS idx_collections_updated_at ON collections(updated_at);
-- name: CreateCollectionTemplatesLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_templates_lookup ON collection_templates(collection_id, site_id);
-- name: CreateCollectionTemplatesDefaultIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_templates_default ON collection_templates(collection_id, site_id, is_default DESC);
-- name: CreateCollectionItemsLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_items_lookup ON collection_items(collection_id, site_id, position);
-- name: CreateCollectionItemsTemplateIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_items_template ON collection_items(template_id);
-- name: CreateCollectionItemVersionsLookupIndex :exec
CREATE INDEX IF NOT EXISTS idx_collection_item_versions_lookup ON collection_item_versions(item_id, collection_id, site_id, created_at DESC);
-- name: CreateCollectionTemplatesOneDefaultIndex :exec
CREATE UNIQUE INDEX IF NOT EXISTS idx_collection_templates_one_default
ON collection_templates(collection_id, site_id)
WHERE is_default = 1;
-- name: CreateUpdateTrigger :exec -- name: CreateUpdateTrigger :exec
CREATE TRIGGER IF NOT EXISTS update_content_updated_at CREATE TRIGGER IF NOT EXISTS update_content_updated_at
AFTER UPDATE ON content AFTER UPDATE ON content
@@ -37,3 +116,19 @@ FOR EACH ROW
BEGIN BEGIN
UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id; UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id;
END; END;
-- name: CreateCollectionsUpdateTrigger :exec
CREATE TRIGGER IF NOT EXISTS update_collections_updated_at
AFTER UPDATE ON collections
FOR EACH ROW
BEGIN
UPDATE collections SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id;
END;
-- name: CreateCollectionItemsUpdateTrigger :exec
CREATE TRIGGER IF NOT EXISTS update_collection_items_updated_at
AFTER UPDATE ON collection_items
FOR EACH ROW
BEGIN
UPDATE collection_items SET updated_at = strftime('%s', 'now') WHERE item_id = NEW.item_id AND collection_id = NEW.collection_id AND site_id = NEW.site_id;
END;

View File

@@ -9,6 +9,83 @@ import (
"context" "context"
) )
const initializeCollectionItemVersionsTable = `-- name: InitializeCollectionItemVersionsTable :exec
CREATE TABLE IF NOT EXISTS collection_item_versions (
version_id INTEGER PRIMARY KEY AUTOINCREMENT,
item_id TEXT NOT NULL,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
html_content TEXT NOT NULL,
template_id INTEGER NOT NULL,
position INTEGER NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
created_by TEXT DEFAULT 'system' NOT NULL,
FOREIGN KEY (item_id, collection_id, site_id) REFERENCES collection_items(item_id, collection_id, site_id) ON DELETE CASCADE
)
`
func (q *Queries) InitializeCollectionItemVersionsTable(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, initializeCollectionItemVersionsTable)
return err
}
const initializeCollectionItemsTable = `-- name: InitializeCollectionItemsTable :exec
CREATE TABLE IF NOT EXISTS collection_items (
item_id TEXT NOT NULL,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
template_id INTEGER NOT NULL,
html_content TEXT NOT NULL,
position INTEGER NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (item_id, collection_id, site_id),
FOREIGN KEY (collection_id, site_id) REFERENCES collections(id, site_id) ON DELETE CASCADE,
FOREIGN KEY (template_id) REFERENCES collection_templates(template_id) ON DELETE RESTRICT
)
`
func (q *Queries) InitializeCollectionItemsTable(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, initializeCollectionItemsTable)
return err
}
const initializeCollectionTemplatesTable = `-- name: InitializeCollectionTemplatesTable :exec
CREATE TABLE IF NOT EXISTS collection_templates (
template_id INTEGER PRIMARY KEY AUTOINCREMENT,
collection_id TEXT NOT NULL,
site_id TEXT NOT NULL,
name TEXT NOT NULL,
html_template TEXT NOT NULL,
is_default INTEGER DEFAULT 0 NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
FOREIGN KEY (collection_id, site_id) REFERENCES collections(id, site_id) ON DELETE CASCADE
)
`
func (q *Queries) InitializeCollectionTemplatesTable(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, initializeCollectionTemplatesTable)
return err
}
const initializeCollectionsTable = `-- name: InitializeCollectionsTable :exec
CREATE TABLE IF NOT EXISTS collections (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
container_html TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
)
`
func (q *Queries) InitializeCollectionsTable(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, initializeCollectionsTable)
return err
}
const initializeSchema = `-- name: InitializeSchema :exec const initializeSchema = `-- name: InitializeSchema :exec
CREATE TABLE IF NOT EXISTS content ( CREATE TABLE IF NOT EXISTS content (
id TEXT NOT NULL, id TEXT NOT NULL,

View File

@@ -217,3 +217,196 @@ func toNullString(s string) sql.NullString {
} }
return sql.NullString{String: s, Valid: true} return sql.NullString{String: s, Valid: true}
} }
// GetCollection retrieves a collection container
func (c *DatabaseClient) GetCollection(siteID, collectionID string) (*CollectionItem, error) {
switch c.database.GetDBType() {
case "sqlite3":
collection, err := c.database.GetSQLiteQueries().GetCollection(context.Background(), sqlite.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
case "postgresql":
collection, err := c.database.GetPostgreSQLQueries().GetCollection(context.Background(), 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
default:
return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType())
}
}
// CreateCollection creates a new collection container
func (c *DatabaseClient) CreateCollection(siteID, collectionID, containerHTML, lastEditedBy string) (*CollectionItem, error) {
switch c.database.GetDBType() {
case "sqlite3":
collection, err := c.database.GetSQLiteQueries().CreateCollection(context.Background(), sqlite.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
case "postgresql":
collection, err := c.database.GetPostgreSQLQueries().CreateCollection(context.Background(), 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
default:
return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType())
}
}
// GetCollectionItems retrieves all items in a collection with template information
func (c *DatabaseClient) GetCollectionItems(siteID, collectionID string) ([]CollectionItemWithTemplate, error) {
switch c.database.GetDBType() {
case "sqlite3":
items, err := c.database.GetSQLiteQueries().GetCollectionItemsWithTemplate(context.Background(), sqlite.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 != 0, // SQLite uses INTEGER for boolean
}
}
return result, nil
case "postgresql":
items, err := c.database.GetPostgreSQLQueries().GetCollectionItemsWithTemplate(context.Background(), 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
default:
return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType())
}
}
// CreateCollectionTemplate creates a new template for a collection
func (c *DatabaseClient) CreateCollectionTemplate(siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error) {
switch c.database.GetDBType() {
case "sqlite3":
var isDefaultInt int64
if isDefault {
isDefaultInt = 1
}
template, err := c.database.GetSQLiteQueries().CreateCollectionTemplate(context.Background(), sqlite.CreateCollectionTemplateParams{
CollectionID: collectionID,
SiteID: siteID,
Name: name,
HtmlTemplate: htmlTemplate,
IsDefault: isDefaultInt,
})
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 != 0,
}, nil
case "postgresql":
template, err := c.database.GetPostgreSQLQueries().CreateCollectionTemplate(context.Background(), 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
default:
return nil, fmt.Errorf("unsupported database type: %s", c.database.GetDBType())
}
}

View File

@@ -52,14 +52,14 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
return nil, fmt.Errorf("parsing HTML: %w", err) return nil, fmt.Errorf("parsing HTML: %w", err)
} }
// 2. Find insertr elements // 2. Find insertr and collection elements
elements := e.findInsertrElements(doc) insertrElements, collectionElements := e.findEditableElements(doc)
// 3. Generate IDs for elements // 3. Process regular .insertr elements
generatedIDs := make(map[string]string) generatedIDs := make(map[string]string)
processedElements := make([]ProcessedElement, len(elements)) processedElements := make([]ProcessedElement, len(insertrElements))
for i, elem := range elements { for i, elem := range insertrElements {
// Generate structural ID (always deterministic) // Generate structural ID (always deterministic)
id := e.idGenerator.Generate(elem.Node, input.FilePath) id := e.idGenerator.Generate(elem.Node, input.FilePath)
@@ -97,7 +97,26 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
} }
} }
// 4. Inject content if required by mode // 4. Process .insertr-add collection elements
for _, collectionElem := range collectionElements {
// Generate structural ID for the collection container
collectionID := e.idGenerator.Generate(collectionElem.Node, input.FilePath)
// Add data-content-id attribute to the collection container
e.setAttribute(collectionElem.Node, "data-content-id", collectionID)
// Process collection during enhancement or content injection
if input.Mode == Enhancement || input.Mode == ContentInjection {
err := e.processCollection(collectionElem.Node, collectionID, input.SiteID)
if err != nil {
fmt.Printf("⚠️ Failed to process collection %s: %v\n", collectionID, err)
} else {
fmt.Printf("✅ Processed collection: %s\n", collectionID)
}
}
}
// 5. Inject content if required by mode
if input.Mode == Enhancement || input.Mode == ContentInjection { if input.Mode == Enhancement || input.Mode == ContentInjection {
err = e.injectContent(processedElements, input.SiteID) err = e.injectContent(processedElements, input.SiteID)
if err != nil { if err != nil {
@@ -105,7 +124,9 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
} }
} }
// 5. Inject editor assets for enhancement mode (development) // TODO: Implement collection-specific content injection here if needed
// 6. Inject editor assets for enhancement mode (development)
if input.Mode == Enhancement { if input.Mode == Enhancement {
injector := NewInjectorWithAuth(e.client, input.SiteID, e.authProvider) injector := NewInjectorWithAuth(e.client, input.SiteID, e.authProvider)
injector.InjectEditorAssets(doc, true, "") injector.InjectEditorAssets(doc, true, "")
@@ -123,31 +144,40 @@ type InsertrElement struct {
Node *html.Node Node *html.Node
} }
// findInsertrElements finds all elements with class="insertr" and applies container transformation // CollectionElement represents an insertr-add collection element found in HTML
// This implements the "syntactic sugar transformation" from CLASSES.md: type CollectionElement struct {
// - Containers with .insertr get their .insertr class removed Node *html.Node
// - Viable children of those containers get .insertr class added }
// - Regular elements with .insertr are kept as-is
func (e *ContentEngine) findInsertrElements(doc *html.Node) []InsertrElement { // findEditableElements finds all editable elements (.insertr and .insertr-add)
var elements []InsertrElement func (e *ContentEngine) findEditableElements(doc *html.Node) ([]InsertrElement, []CollectionElement) {
var insertrElements []InsertrElement
var collectionElements []CollectionElement
var containersToTransform []*html.Node var containersToTransform []*html.Node
// First pass: find all .insertr elements and identify containers // First pass: find all .insertr and .insertr-add elements
e.walkNodes(doc, func(n *html.Node) { e.walkNodes(doc, func(n *html.Node) {
if n.Type == html.ElementNode && e.hasInsertrClass(n) { if n.Type == html.ElementNode {
if isContainer(n) { if e.hasInsertrClass(n) {
// Container element - mark for transformation if isContainer(n) {
containersToTransform = append(containersToTransform, n) // Container element - mark for transformation
} else { containersToTransform = append(containersToTransform, n)
// Regular element - add directly } else {
elements = append(elements, InsertrElement{ // Regular element - add directly
insertrElements = append(insertrElements, InsertrElement{
Node: n,
})
}
} else if e.hasInsertrAddClass(n) {
// Collection element - add directly (no container transformation for collections)
collectionElements = append(collectionElements, CollectionElement{
Node: n, Node: n,
}) })
} }
} }
}) })
// Second pass: transform containers (remove .insertr from container, add to children) // Second pass: transform .insertr containers (remove .insertr from container, add to children)
for _, container := range containersToTransform { for _, container := range containersToTransform {
// Remove .insertr class from container // Remove .insertr class from container
e.removeClass(container, "insertr") e.removeClass(container, "insertr")
@@ -156,13 +186,23 @@ func (e *ContentEngine) findInsertrElements(doc *html.Node) []InsertrElement {
viableChildren := FindViableChildren(container) viableChildren := FindViableChildren(container)
for _, child := range viableChildren { for _, child := range viableChildren {
e.addClass(child, "insertr") e.addClass(child, "insertr")
elements = append(elements, InsertrElement{ insertrElements = append(insertrElements, InsertrElement{
Node: child, Node: child,
}) })
} }
} }
return elements return insertrElements, collectionElements
}
// findInsertrElements finds all elements with class="insertr" and applies container transformation
// This implements the "syntactic sugar transformation" from CLASSES.md:
// - Containers with .insertr get their .insertr class removed
// - Viable children of those containers get .insertr class added
// - Regular elements with .insertr are kept as-is
func (e *ContentEngine) findInsertrElements(doc *html.Node) []InsertrElement {
insertrElements, _ := e.findEditableElements(doc)
return insertrElements
} }
// walkNodes walks through all nodes in the document // walkNodes walks through all nodes in the document
@@ -184,6 +224,17 @@ func (e *ContentEngine) hasInsertrClass(node *html.Node) bool {
return false return false
} }
// hasInsertrAddClass checks if node has class="insertr-add" (collection)
func (e *ContentEngine) hasInsertrAddClass(node *html.Node) bool {
classes := GetClasses(node)
for _, class := range classes {
if class == "insertr-add" {
return true
}
}
return false
}
// addContentAttributes adds data-content-id attribute only // addContentAttributes adds data-content-id attribute only
// HTML-first approach: no content-type attribute needed // HTML-first approach: no content-type attribute needed
func (e *ContentEngine) addContentAttributes(node *html.Node, contentID string) { func (e *ContentEngine) addContentAttributes(node *html.Node, contentID string) {
@@ -342,3 +393,124 @@ func (e *ContentEngine) extractOriginalTemplate(node *html.Node) string {
} }
return buf.String() return buf.String()
} }
// processCollection handles collection detection, persistence and reconstruction
func (e *ContentEngine) processCollection(collectionNode *html.Node, collectionID, siteID string) error {
// 1. Check if collection exists in database
existingCollection, err := e.client.GetCollection(siteID, collectionID)
collectionExists := (err == nil && existingCollection != nil)
if !collectionExists {
// 2. New collection: extract container HTML and create collection record
containerHTML := e.extractOriginalTemplate(collectionNode)
_, err := e.client.CreateCollection(siteID, collectionID, containerHTML, "system")
if err != nil {
return fmt.Errorf("failed to create collection %s: %w", collectionID, err)
}
// 3. Extract templates from existing children
err = e.extractAndStoreTemplates(collectionNode, collectionID, siteID)
if err != nil {
return fmt.Errorf("failed to extract templates for collection %s: %w", collectionID, err)
}
fmt.Printf("✅ Created new collection: %s with templates\n", collectionID)
} else {
// 4. Existing collection: reconstruct items from database
err = e.reconstructCollectionItems(collectionNode, collectionID, siteID)
if err != nil {
return fmt.Errorf("failed to reconstruct collection %s: %w", collectionID, err)
}
fmt.Printf("✅ Reconstructed collection: %s from database\n", collectionID)
}
return nil
}
// extractAndStoreTemplates extracts template patterns from existing collection children
func (e *ContentEngine) extractAndStoreTemplates(collectionNode *html.Node, collectionID, siteID string) error {
// Find existing children elements to use as templates
var templateElements []*html.Node
// Walk through direct children of the collection
for child := collectionNode.FirstChild; child != nil; child = child.NextSibling {
if child.Type == html.ElementNode {
templateElements = append(templateElements, child)
}
}
if len(templateElements) == 0 {
// No existing children - create a default empty template
_, err := e.client.CreateCollectionTemplate(siteID, collectionID, "default", "<div>New item</div>", true)
if err != nil {
return fmt.Errorf("failed to create default template: %w", err)
}
fmt.Printf("✅ Created default template for collection %s\n", collectionID)
return nil
}
// Extract templates from existing children
for i, templateElement := range templateElements {
templateHTML := e.extractOriginalTemplate(templateElement)
templateName := fmt.Sprintf("template-%d", i+1)
isDefault := (i == 0) // First template is default
_, err := e.client.CreateCollectionTemplate(siteID, collectionID, templateName, templateHTML, isDefault)
if err != nil {
return fmt.Errorf("failed to create template %s: %w", templateName, err)
}
fmt.Printf("✅ Created template '%s' for collection %s\n", templateName, collectionID)
}
return nil
}
// reconstructCollectionItems rebuilds collection items from database and adds them to DOM
func (e *ContentEngine) reconstructCollectionItems(collectionNode *html.Node, collectionID, siteID string) error {
// Get all items for this collection from database
items, err := e.client.GetCollectionItems(siteID, collectionID)
if err != nil {
return fmt.Errorf("failed to get collection items: %w", err)
}
// Clear existing children (they will be replaced with database items)
for child := collectionNode.FirstChild; child != nil; {
next := child.NextSibling
collectionNode.RemoveChild(child)
child = next
}
// Add items from database in position order
for _, item := range items {
// Parse the item HTML content
itemDoc, err := html.Parse(strings.NewReader(item.HTMLContent))
if err != nil {
fmt.Printf("⚠️ Failed to parse item HTML for %s: %v\n", item.ItemID, err)
continue
}
// Find the body element and extract its children
var bodyNode *html.Node
e.walkNodes(itemDoc, func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "body" {
bodyNode = n
}
})
if bodyNode != nil {
// Move all children from body to collection
for bodyChild := bodyNode.FirstChild; bodyChild != nil; {
next := bodyChild.NextSibling
bodyNode.RemoveChild(bodyChild)
collectionNode.AppendChild(bodyChild)
bodyChild = next
}
}
}
fmt.Printf("✅ Reconstructed %d items for collection %s\n", len(items), collectionID)
return nil
}

View File

@@ -48,6 +48,12 @@ type ContentClient interface {
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) GetAllContent(siteID string) (map[string]ContentItem, error)
CreateContent(siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*ContentItem, error) CreateContent(siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*ContentItem, error)
// Collection operations
GetCollection(siteID, collectionID string) (*CollectionItem, error)
CreateCollection(siteID, collectionID, containerHTML, lastEditedBy string) (*CollectionItem, error)
GetCollectionItems(siteID, collectionID string) ([]CollectionItemWithTemplate, error)
CreateCollectionTemplate(siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error)
} }
// ContentItem represents a piece of content from the database // ContentItem represents a piece of content from the database
@@ -65,3 +71,39 @@ type ContentResponse struct {
Content []ContentItem `json:"content"` Content []ContentItem `json:"content"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
// CollectionItem represents a collection container from the database
type CollectionItem struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
ContainerHTML string `json:"container_html"`
UpdatedAt string `json:"updated_at"`
LastEditedBy string `json:"last_edited_by,omitempty"`
}
// CollectionTemplateItem represents a collection template from the database
type CollectionTemplateItem struct {
TemplateID int `json:"template_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
Name string `json:"name"`
HTMLTemplate string `json:"html_template"`
IsDefault bool `json:"is_default"`
}
// CollectionItemWithTemplate represents a collection item with its template information
type CollectionItemWithTemplate struct {
ItemID string `json:"item_id"`
CollectionID string `json:"collection_id"`
SiteID string `json:"site_id"`
TemplateID int `json:"template_id"`
HTMLContent string `json:"html_content"`
Position int `json:"position"`
UpdatedAt string `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
// Template information
TemplateName string `json:"template_name"`
HTMLTemplate string `json:"html_template"`
IsDefault bool `json:"is_default"`
}

View File

@@ -1,4 +1,5 @@
import { InsertrFormRenderer } from '../ui/form-renderer.js'; import { InsertrFormRenderer } from '../ui/form-renderer.js';
import { CollectionManager } from '../ui/collection-manager.js';
/** /**
* InsertrEditor - Content editing workflow and business logic * InsertrEditor - Content editing workflow and business logic
@@ -67,8 +68,18 @@ export class InsertrEditor {
initializeElement(meta) { initializeElement(meta) {
const { element } = meta; const { element } = meta;
// Add click handler for editing if (meta.editorType === 'collection') {
this.addClickHandler(element, meta); // Initialize collection management
this.initializeCollection(meta);
} else {
// Add click handler for editing individual elements
this.addClickHandler(element, meta);
}
}
initializeCollection(meta) {
const collectionManager = new CollectionManager(meta, this.apiClient, this.auth);
collectionManager.initialize();
} }
addClickHandler(element, meta) { addClickHandler(element, meta) {

View File

@@ -12,17 +12,19 @@ export class InsertrCore {
// Find all enhanced elements on the page // Find all enhanced elements on the page
findEnhancedElements() { findEnhancedElements() {
return document.querySelectorAll('.insertr'); return document.querySelectorAll('.insertr, .insertr-add');
} }
// Get element metadata // Get element metadata
getElementMetadata(element) { getElementMetadata(element) {
const existingId = element.getAttribute('data-content-id'); const existingId = element.getAttribute('data-content-id');
const isCollection = element.classList.contains('insertr-add');
return { return {
contentId: existingId, contentId: existingId,
element: element, element: element,
htmlMarkup: element.outerHTML // Server will generate ID from this htmlMarkup: element.outerHTML, // Server will generate ID from this
editorType: isCollection ? 'collection' : 'element'
}; };
} }

View File

@@ -828,3 +828,129 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
order: -1; order: -1;
} }
} }
/* =================================================================
COLLECTION MANAGEMENT STYLES (.insertr-add)
================================================================= */
/* Collection container when active */
.insertr-collection-active {
outline: 2px dashed var(--insertr-primary);
outline-offset: 4px;
border-radius: var(--insertr-border-radius);
}
/* Add button positioned in top right of container */
.insertr-add-btn {
position: absolute;
top: -12px;
right: -12px;
background: var(--insertr-primary);
color: var(--insertr-text-inverse);
border: none;
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
border-radius: var(--insertr-border-radius);
font-size: var(--insertr-font-size-sm);
font-weight: 600;
cursor: pointer;
z-index: var(--insertr-z-floating);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.insertr-add-btn:hover {
background: var(--insertr-primary-hover);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.insertr-add-btn:active {
transform: translateY(0);
}
/* Item controls positioned in top right corner of each item */
.insertr-item-controls {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 2px;
opacity: 0;
transition: opacity 0.2s ease;
z-index: var(--insertr-z-floating);
}
/* Individual control buttons */
.insertr-control-btn {
width: 20px;
height: 20px;
background: var(--insertr-bg-primary);
border: 1px solid var(--insertr-border-color);
border-radius: 3px;
font-size: 12px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--insertr-text-primary);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.15s ease;
}
.insertr-control-btn:hover {
background: var(--insertr-bg-secondary);
border-color: var(--insertr-primary);
color: var(--insertr-primary);
transform: scale(1.1);
}
/* Remove button specific styling */
.insertr-control-btn:last-child {
color: var(--insertr-danger);
}
.insertr-control-btn:last-child:hover {
background: var(--insertr-danger);
color: var(--insertr-text-inverse);
border-color: var(--insertr-danger);
}
/* Collection items hover state */
.insertr-collection-active > *:hover {
background: rgba(0, 123, 255, 0.03);
outline: 1px solid rgba(var(--insertr-primary), 0.2);
outline-offset: 2px;
border-radius: var(--insertr-border-radius);
}
/* Show item controls on hover */
.insertr-collection-active > *:hover .insertr-item-controls {
opacity: 1;
}
/* Responsive adjustments for collection management */
@media (max-width: 768px) {
.insertr-add-btn {
position: static;
display: block;
margin: var(--insertr-spacing-sm) auto 0;
width: 100%;
max-width: 200px;
}
.insertr-item-controls {
position: relative;
opacity: 1;
top: auto;
right: auto;
justify-content: center;
margin-top: var(--insertr-spacing-xs);
}
.insertr-control-btn {
width: 32px;
height: 32px;
font-size: 14px;
}
}

View File

@@ -0,0 +1,610 @@
/**
* CollectionManager - Dynamic content collection management for .insertr-add elements
*
* Handles:
* - Template detection from existing children
* - Add/remove/reorder UI controls
* - Collection data management
* - Integration with existing .insertr editing system
*/
import { InsertrFormRenderer } from './form-renderer.js';
export class CollectionManager {
constructor(meta, apiClient, auth) {
this.meta = meta;
this.container = meta.element;
this.apiClient = apiClient;
this.auth = auth;
// Collection state
this.template = null;
this.items = [];
this.isActive = false;
// UI elements
this.addButton = null;
this.itemControls = new Map(); // Map item element to its controls
console.log('🔄 CollectionManager initialized for:', this.container);
}
/**
* Initialize the collection manager
*/
initialize() {
if (this.isActive) return;
console.log('🚀 Starting collection management for:', this.container.className);
// Analyze existing content to detect template
this.analyzeTemplate();
// Add collection management UI only when in edit mode
this.setupEditModeDetection();
this.isActive = true;
}
/**
* Set up detection for when edit mode is activated
*/
setupEditModeDetection() {
// Check current auth state
if (this.auth.isAuthenticated() && this.auth.isEditMode()) {
this.activateCollectionUI();
}
// Listen for auth state changes (assuming the auth object has events)
// For now, we'll poll - in a real implementation we'd use events
this.authCheckInterval = setInterval(() => {
const shouldBeActive = this.auth.isAuthenticated() && this.auth.isEditMode();
if (shouldBeActive && !this.hasCollectionUI()) {
this.activateCollectionUI();
} else if (!shouldBeActive && this.hasCollectionUI()) {
this.deactivateCollectionUI();
}
}, 1000);
}
/**
* Check if collection UI is currently active
*/
hasCollectionUI() {
return this.addButton && this.addButton.parentNode;
}
/**
* Activate collection UI when in edit mode
*/
activateCollectionUI() {
console.log('✅ Activating collection UI');
// Add visual indicator to container
this.container.classList.add('insertr-collection-active');
// Add the "+ Add" button (top right of container per spec)
this.createAddButton();
// Add control buttons to each existing item
this.addControlsToExistingItems();
}
/**
* Deactivate collection UI when not in edit mode
*/
deactivateCollectionUI() {
console.log('❌ Deactivating collection UI');
// Remove visual indicator
this.container.classList.remove('insertr-collection-active');
// Remove add button
if (this.addButton) {
this.addButton.remove();
this.addButton = null;
}
// Remove all item controls
this.itemControls.forEach((controls, item) => {
controls.remove();
});
this.itemControls.clear();
}
/**
* Analyze existing children to detect template pattern
*/
analyzeTemplate() {
const children = Array.from(this.container.children);
if (children.length === 0) {
console.warn('⚠️ No children found for template analysis');
return;
}
// Use first child as template baseline
const firstChild = children[0];
this.template = {
structure: this.extractElementStructure(firstChild),
editableFields: this.findEditableElements(firstChild),
htmlTemplate: firstChild.outerHTML
};
console.log('📋 Template detected:', this.template);
// Store reference to current items
this.items = children.map((child, index) => ({
element: child,
index: index,
id: this.generateItemId(index)
}));
}
/**
* Extract the structural pattern of an element
*/
extractElementStructure(element) {
return {
tagName: element.tagName,
classes: Array.from(element.classList),
attributes: this.getRelevantAttributes(element),
childStructure: this.analyzeChildStructure(element)
};
}
/**
* Get relevant attributes (excluding data-content-id which will be unique)
*/
getRelevantAttributes(element) {
const relevantAttrs = {};
for (const attr of element.attributes) {
if (attr.name !== 'data-content-id') {
relevantAttrs[attr.name] = attr.value;
}
}
return relevantAttrs;
}
/**
* Analyze child structure for template replication
*/
analyzeChildStructure(element) {
return Array.from(element.children).map(child => ({
tagName: child.tagName,
classes: Array.from(child.classList),
hasInsertrClass: child.classList.contains('insertr'),
content: child.classList.contains('insertr') ? '' : child.textContent
}));
}
/**
* Find editable elements within a container
*/
findEditableElements(container) {
return Array.from(container.querySelectorAll('.insertr')).map(el => ({
selector: this.generateRelativeSelector(el, container),
type: this.determineFieldType(el),
placeholder: this.generatePlaceholder(el)
}));
}
/**
* Generate a relative selector for an element within a container
*/
generateRelativeSelector(element, container) {
// Simple approach: use tag name and classes
const tagName = element.tagName.toLowerCase();
const classes = Array.from(element.classList).join('.');
return classes ? `${tagName}.${classes}` : tagName;
}
/**
* Determine the type of field for editing
*/
determineFieldType(element) {
const tagName = element.tagName.toLowerCase();
if (tagName === 'a') return 'link';
if (tagName === 'img') return 'image';
return 'text';
}
/**
* Generate placeholder text for empty fields
*/
generatePlaceholder(element) {
const tagName = element.tagName.toLowerCase();
if (tagName === 'h1' || tagName === 'h2') return 'Enter heading...';
if (tagName === 'blockquote') return 'Enter quote...';
if (tagName === 'cite') return 'Enter author...';
return 'Enter text...';
}
/**
* Generate unique ID for new items
*/
generateItemId(index) {
return `item-${Date.now()}-${index}`;
}
/**
* Create the "+ Add" button positioned in top right of container
*/
createAddButton() {
if (this.addButton) return; // Already exists
this.addButton = document.createElement('button');
this.addButton.className = 'insertr-add-btn';
this.addButton.innerHTML = '+ Add Item';
this.addButton.title = 'Add new item to collection';
// Position in top right of container as per spec
this.container.style.position = 'relative';
this.addButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.addNewItem();
});
this.container.appendChild(this.addButton);
console.log(' Add button created');
}
/**
* Add control buttons to all existing items
*/
addControlsToExistingItems() {
this.items.forEach((item, index) => {
this.addItemControls(item.element, index);
});
}
/**
* Add management controls to an item (remove, reorder)
*/
addItemControls(itemElement, index) {
if (this.itemControls.has(itemElement)) return; // Already has controls
const controls = document.createElement('div');
controls.className = 'insertr-item-controls';
// Remove button (always present)
const removeBtn = this.createControlButton('×', 'Remove item', () =>
this.removeItem(itemElement)
);
// Move up button (if not first item)
if (index > 0) {
const upBtn = this.createControlButton('↑', 'Move up', () =>
this.moveItem(itemElement, 'up')
);
controls.appendChild(upBtn);
}
// Move down button (if not last item)
if (index < this.items.length - 1) {
const downBtn = this.createControlButton('↓', 'Move down', () =>
this.moveItem(itemElement, 'down')
);
controls.appendChild(downBtn);
}
controls.appendChild(removeBtn);
// Position in top right corner of item as per spec
itemElement.style.position = 'relative';
itemElement.appendChild(controls);
// Store reference
this.itemControls.set(itemElement, controls);
// Add hover behavior
this.setupItemHoverBehavior(itemElement, controls);
}
/**
* Create a control button
*/
createControlButton(text, title, onClick) {
const button = document.createElement('button');
button.className = 'insertr-control-btn';
button.textContent = text;
button.title = title;
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
onClick();
});
return button;
}
/**
* Set up hover behavior for item controls
*/
setupItemHoverBehavior(itemElement, controls) {
itemElement.addEventListener('mouseenter', () => {
controls.style.opacity = '1';
});
itemElement.addEventListener('mouseleave', () => {
controls.style.opacity = '0';
});
}
/**
* Add a new item to the collection
*/
addNewItem() {
console.log(' Adding new item to collection');
if (!this.template) {
console.error('❌ No template available for creating new items');
return;
}
// Create new item from template
const newItem = this.createItemFromTemplate();
// Add to DOM
this.container.insertBefore(newItem, this.addButton);
// Update items array
const newItemData = {
element: newItem,
index: this.items.length,
id: this.generateItemId(this.items.length)
};
this.items.push(newItemData);
// Add controls to new item
this.addItemControls(newItem, this.items.length - 1);
// Re-initialize any .insertr elements in the new item
// This allows the existing editor system to handle individual field editing
this.initializeInsertrElements(newItem);
// Update all item controls (indices may have changed)
this.updateAllItemControls();
console.log('✅ New item added successfully');
}
/**
* Create a new item from the template
*/
createItemFromTemplate() {
// Create element from template HTML
const tempContainer = document.createElement('div');
tempContainer.innerHTML = this.template.htmlTemplate;
const newItem = tempContainer.firstElementChild;
// Clear content from editable fields
this.template.editableFields.forEach(field => {
const element = newItem.querySelector(field.selector);
if (element) {
this.clearElementContent(element, field.type);
// Add placeholder text
if (field.type === 'text') {
element.textContent = field.placeholder;
element.style.color = '#999';
element.style.fontStyle = 'italic';
// Remove placeholder styling when user starts editing
element.addEventListener('focus', () => {
if (element.textContent === field.placeholder) {
element.textContent = '';
element.style.color = '';
element.style.fontStyle = '';
}
});
}
}
});
// Generate unique data-content-id for the item
newItem.setAttribute('data-content-id', this.generateItemId(Date.now()));
return newItem;
}
/**
* Clear content from an element based on its type
*/
clearElementContent(element, type) {
if (type === 'link') {
element.textContent = '';
element.removeAttribute('href');
} else if (type === 'image') {
element.removeAttribute('src');
element.removeAttribute('alt');
} else {
element.textContent = '';
}
}
/**
* Initialize .insertr elements within a new item
* This integrates with the existing editing system
*/
initializeInsertrElements(container) {
const insertrElements = container.querySelectorAll('.insertr');
insertrElements.forEach(element => {
// Add click handler for editing (same as existing system)
element.addEventListener('click', (e) => {
// Only allow editing if authenticated and in edit mode
if (!this.auth.isAuthenticated() || !this.auth.isEditMode()) {
return;
}
e.preventDefault();
e.stopPropagation();
// Use the existing form renderer
const formRenderer = new InsertrFormRenderer(this.apiClient);
const meta = {
contentId: element.getAttribute('data-content-id'),
element: element,
htmlMarkup: element.outerHTML
};
const currentContent = this.extractCurrentContent(element);
formRenderer.showEditForm(
meta,
currentContent,
(formData) => this.handleItemSave(meta, formData),
() => formRenderer.closeForm()
);
});
});
}
/**
* Extract current content (simplified version of editor.js method)
*/
extractCurrentContent(element) {
if (element.tagName.toLowerCase() === 'a') {
return {
text: element.textContent.trim(),
url: element.getAttribute('href') || ''
};
}
return element.textContent.trim();
}
/**
* Handle saving of individual item content
*/
async handleItemSave(meta, formData) {
console.log('💾 Saving item content:', meta.contentId, formData);
try {
let contentValue;
if (typeof formData === 'string') {
contentValue = formData;
} else if (formData.content) {
contentValue = formData.content;
} else if (formData.text) {
contentValue = formData.text;
} else {
contentValue = formData;
}
let result;
if (meta.contentId) {
result = await this.apiClient.updateContent(meta.contentId, contentValue);
} else {
result = await this.apiClient.createContent(contentValue, meta.htmlMarkup);
}
if (result) {
meta.element.setAttribute('data-content-id', result.id);
console.log(`✅ Item content saved: ${result.id}`);
} else {
console.error('❌ Failed to save item content to server');
}
} catch (error) {
console.error('❌ Error saving item content:', error);
}
}
/**
* Remove an item from the collection
*/
removeItem(itemElement) {
if (!confirm('Are you sure you want to remove this item?')) {
return;
}
console.log('🗑️ Removing item from collection');
// Remove controls
const controls = this.itemControls.get(itemElement);
if (controls) {
controls.remove();
this.itemControls.delete(itemElement);
}
// Remove from items array
this.items = this.items.filter(item => item.element !== itemElement);
// Remove from DOM
itemElement.remove();
// Update all item controls (indices changed)
this.updateAllItemControls();
console.log('✅ Item removed successfully');
}
/**
* Move an item up or down in the collection
*/
moveItem(itemElement, direction) {
console.log(`🔄 Moving item ${direction}`);
const currentIndex = this.items.findIndex(item => item.element === itemElement);
if (currentIndex === -1) return;
let newIndex;
if (direction === 'up' && currentIndex > 0) {
newIndex = currentIndex - 1;
} else if (direction === 'down' && currentIndex < this.items.length - 1) {
newIndex = currentIndex + 1;
} else {
return; // Can't move in that direction
}
// Get the target position in DOM
const targetItem = this.items[newIndex];
// Move in DOM
if (direction === 'up') {
this.container.insertBefore(itemElement, targetItem.element);
} else {
this.container.insertBefore(itemElement, targetItem.element.nextSibling);
}
// Update items array
[this.items[currentIndex], this.items[newIndex]] = [this.items[newIndex], this.items[currentIndex]];
this.items[currentIndex].index = currentIndex;
this.items[newIndex].index = newIndex;
// Update all item controls
this.updateAllItemControls();
console.log('✅ Item moved successfully');
}
/**
* Update controls for all items (called after reordering)
*/
updateAllItemControls() {
// Remove all existing controls
this.itemControls.forEach((controls, item) => {
controls.remove();
});
this.itemControls.clear();
// Re-add controls with correct up/down button states
this.items.forEach((item, index) => {
this.addItemControls(item.element, index);
});
}
/**
* Cleanup when the collection manager is destroyed
*/
destroy() {
if (this.authCheckInterval) {
clearInterval(this.authCheckInterval);
}
this.deactivateCollectionUI();
this.isActive = false;
console.log('🧹 CollectionManager destroyed');
}
}