diff --git a/cmd/enhance.go b/cmd/enhance.go index 8b9f3ab..d3b3dd6 100644 --- a/cmd/enhance.go +++ b/cmd/enhance.go @@ -102,7 +102,7 @@ func runEnhance(cmd *cobra.Command, args []string) { log.Fatalf("Failed to initialize database: %v", err) } defer database.Close() - client = content.NewDatabaseClient(database) + client = engine.NewDatabaseClient(database) } else { fmt.Printf("๐Ÿงช No database or API configured, using mock content\n") client = content.NewMockClient() diff --git a/demos/simple/index.html b/demos/simple/index.html index 13ed127..008948d 100644 --- a/demos/simple/index.html +++ b/demos/simple/index.html @@ -95,6 +95,53 @@ font-size: 1.25rem; 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: "โ€” "; + } @@ -166,5 +213,33 @@

Need help? Contact our support team anytime.

+ + +
+

Example 8: Dynamic Collection (.insertr-add)

+
+ Tests dynamic add/remove/reorder functionality. In edit mode, you should see "+ Add Item" button and item controls. +
+
+
+
Not all that is gold does glitter
+ Tolkien +
+
+
The journey of a thousand miles begins with one step
+ Lao Tzu +
+
+
Innovation distinguishes between a leader and a follower
+ Steve Jobs +
+
+
+ + + diff --git a/internal/content/assets/insertr.css b/internal/content/assets/insertr.css index 1fe0049..31e433e 100644 --- a/internal/content/assets/insertr.css +++ b/internal/content/assets/insertr.css @@ -827,4 +827,130 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after { margin-right: 0; 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; + } } \ No newline at end of file diff --git a/internal/content/client.go b/internal/content/client.go index 5907f54..141a30e 100644 --- a/internal/content/client.go +++ b/internal/content/client.go @@ -171,3 +171,20 @@ func (c *HTTPClient) CreateContent(siteID, contentID, htmlContent, originalTempl // This would typically be used in API-driven enhancement scenarios 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") +} diff --git a/internal/content/database.go b/internal/content/database.go index 7c55462..cbbfafd 100644 --- a/internal/content/database.go +++ b/internal/content/database.go @@ -233,3 +233,20 @@ func toNullString(s string) sql.NullString { } 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") +} diff --git a/internal/content/mock.go b/internal/content/mock.go index 4bb41b0..ec65534 100644 --- a/internal/content/mock.go +++ b/internal/content/mock.go @@ -1,6 +1,7 @@ package content import ( + "fmt" "time" "github.com/insertr/insertr/internal/engine" @@ -156,3 +157,20 @@ func (m *MockClient) CreateContent(siteID, contentID, htmlContent, originalTempl 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") +} diff --git a/internal/db/database.go b/internal/db/database.go index 2a824d8..83a5f73 100644 --- a/internal/db/database.go +++ b/internal/db/database.go @@ -108,11 +108,36 @@ func (db *Database) initializeSQLiteSchema() error { 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) indexQueries := []string{ "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_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 { @@ -121,17 +146,32 @@ func (db *Database) initializeSQLiteSchema() error { } } - // Create update trigger manually (sqlc doesn't generate trigger creation functions) - triggerQuery := ` - CREATE TRIGGER IF NOT EXISTS update_content_updated_at - AFTER UPDATE ON content - FOR EACH ROW - BEGIN - UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id; - END;` + // Create update triggers manually (sqlc doesn't generate trigger creation functions) + triggerQueries := []string{ + `CREATE TRIGGER IF NOT EXISTS update_content_updated_at + AFTER UPDATE ON content + FOR EACH ROW + BEGIN + UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id; + 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 { - return fmt.Errorf("failed to create update trigger: %w", err) + for _, query := range triggerQueries { + if _, err := db.conn.Exec(query); err != nil { + return fmt.Errorf("failed to create trigger: %w", err) + } } return nil @@ -150,6 +190,23 @@ func (db *Database) initializePostgreSQLSchema() error { 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) if err := db.postgresqlQueries.CreateContentSiteIndex(ctx); err != nil { 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) } + // 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 if err := db.postgresqlQueries.CreateUpdateFunction(ctx); err != nil { return fmt.Errorf("failed to create update function: %w", err) } - // Create trigger manually (sqlc doesn't generate trigger creation functions) - triggerQuery := ` - DROP TRIGGER IF EXISTS update_content_updated_at ON content; + // Create triggers manually (sqlc doesn't generate trigger creation functions) + triggerQueries := []string{ + `DROP TRIGGER IF EXISTS update_content_updated_at ON content; CREATE TRIGGER update_content_updated_at BEFORE UPDATE ON content 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 { - return fmt.Errorf("failed to create update trigger: %w", err) + for _, query := range triggerQueries { + if _, err := db.conn.Exec(query); err != nil { + return fmt.Errorf("failed to create trigger: %w", err) + } } return nil diff --git a/internal/db/postgresql/collection_item_versions.sql.go b/internal/db/postgresql/collection_item_versions.sql.go new file mode 100644 index 0000000..15457c1 --- /dev/null +++ b/internal/db/postgresql/collection_item_versions.sql.go @@ -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 +} diff --git a/internal/db/postgresql/collection_items.sql.go b/internal/db/postgresql/collection_items.sql.go new file mode 100644 index 0000000..0f21aa0 --- /dev/null +++ b/internal/db/postgresql/collection_items.sql.go @@ -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 +} diff --git a/internal/db/postgresql/collection_templates.sql.go b/internal/db/postgresql/collection_templates.sql.go new file mode 100644 index 0000000..2d027dd --- /dev/null +++ b/internal/db/postgresql/collection_templates.sql.go @@ -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 +} diff --git a/internal/db/postgresql/collections.sql.go b/internal/db/postgresql/collections.sql.go new file mode 100644 index 0000000..767a1f7 --- /dev/null +++ b/internal/db/postgresql/collections.sql.go @@ -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 +} diff --git a/internal/db/postgresql/models.go b/internal/db/postgresql/models.go index 57722de..77cc6be 100644 --- a/internal/db/postgresql/models.go +++ b/internal/db/postgresql/models.go @@ -8,6 +8,49 @@ import ( "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 { ID string `json:"id"` SiteID string `json:"site_id"` diff --git a/internal/db/postgresql/querier.go b/internal/db/postgresql/querier.go index 4114516..8c86353 100644 --- a/internal/db/postgresql/querier.go +++ b/internal/db/postgresql/querier.go @@ -9,24 +9,70 @@ import ( ) 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) CreateContentSiteIndex(ctx context.Context) error CreateContentUpdatedAtIndex(ctx context.Context) error CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error CreateUpdateFunction(ctx context.Context) error CreateVersionsLookupIndex(ctx context.Context) error + DeleteAllSiteCollections(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 + DeleteOldCollectionItemVersions(ctx context.Context, arg DeleteOldCollectionItemVersionsParams) 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) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsForSiteParams) ([]GetAllVersionsForSiteRow, 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) GetContentVersion(ctx context.Context, versionID int32) (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 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) + UpsertCollection(ctx context.Context, arg UpsertCollectionParams) (Collection, error) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) } diff --git a/internal/db/postgresql/schema.sql b/internal/db/postgresql/schema.sql index d1799d9..29703ac 100644 --- a/internal/db/postgresql/schema.sql +++ b/internal/db/postgresql/schema.sql @@ -22,11 +22,78 @@ CREATE TABLE content_versions ( 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 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_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 CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ @@ -39,4 +106,15 @@ $$ language 'plpgsql'; CREATE TRIGGER update_content_updated_at BEFORE UPDATE ON content FOR EACH ROW + 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(); \ No newline at end of file diff --git a/internal/db/postgresql/setup.sql b/internal/db/postgresql/setup.sql index db1792b..22643c2 100644 --- a/internal/db/postgresql/setup.sql +++ b/internal/db/postgresql/setup.sql @@ -21,6 +21,59 @@ CREATE TABLE IF NOT EXISTS content_versions ( 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 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 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 CREATE OR REPLACE FUNCTION update_content_timestamp() RETURNS TRIGGER AS $$ @@ -44,4 +123,18 @@ DROP TRIGGER IF EXISTS update_content_updated_at ON content; CREATE TRIGGER update_content_updated_at BEFORE UPDATE ON content FOR EACH ROW +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(); \ No newline at end of file diff --git a/internal/db/postgresql/setup.sql.go b/internal/db/postgresql/setup.sql.go index f8efe47..7cefce4 100644 --- a/internal/db/postgresql/setup.sql.go +++ b/internal/db/postgresql/setup.sql.go @@ -9,6 +9,80 @@ import ( "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 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 } +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 CREATE TABLE IF NOT EXISTS content ( id TEXT NOT NULL, diff --git a/internal/db/queries/collection_item_versions.sql b/internal/db/queries/collection_item_versions.sql new file mode 100644 index 0000000..0e202cf --- /dev/null +++ b/internal/db/queries/collection_item_versions.sql @@ -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); \ No newline at end of file diff --git a/internal/db/queries/collection_items.sql b/internal/db/queries/collection_items.sql new file mode 100644 index 0000000..b1e18b2 --- /dev/null +++ b/internal/db/queries/collection_items.sql @@ -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); + diff --git a/internal/db/queries/collection_templates.sql b/internal/db/queries/collection_templates.sql new file mode 100644 index 0000000..a65e134 --- /dev/null +++ b/internal/db/queries/collection_templates.sql @@ -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); \ No newline at end of file diff --git a/internal/db/queries/collections.sql b/internal/db/queries/collections.sql new file mode 100644 index 0000000..04b6098 --- /dev/null +++ b/internal/db/queries/collections.sql @@ -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); \ No newline at end of file diff --git a/internal/db/sqlite/collection_item_versions.sql.go b/internal/db/sqlite/collection_item_versions.sql.go new file mode 100644 index 0000000..9861b74 --- /dev/null +++ b/internal/db/sqlite/collection_item_versions.sql.go @@ -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 +} diff --git a/internal/db/sqlite/collection_items.sql.go b/internal/db/sqlite/collection_items.sql.go new file mode 100644 index 0000000..ecc0a6b --- /dev/null +++ b/internal/db/sqlite/collection_items.sql.go @@ -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 +} diff --git a/internal/db/sqlite/collection_templates.sql.go b/internal/db/sqlite/collection_templates.sql.go new file mode 100644 index 0000000..99ba3c3 --- /dev/null +++ b/internal/db/sqlite/collection_templates.sql.go @@ -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 +} diff --git a/internal/db/sqlite/collections.sql.go b/internal/db/sqlite/collections.sql.go new file mode 100644 index 0000000..fd01eef --- /dev/null +++ b/internal/db/sqlite/collections.sql.go @@ -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 +} diff --git a/internal/db/sqlite/models.go b/internal/db/sqlite/models.go index afdfd08..55dcf96 100644 --- a/internal/db/sqlite/models.go +++ b/internal/db/sqlite/models.go @@ -8,6 +8,49 @@ import ( "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 { ID string `json:"id"` SiteID string `json:"site_id"` diff --git a/internal/db/sqlite/querier.go b/internal/db/sqlite/querier.go index e52f069..37f8a7c 100644 --- a/internal/db/sqlite/querier.go +++ b/internal/db/sqlite/querier.go @@ -9,20 +9,58 @@ import ( ) 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) CreateContentVersion(ctx context.Context, arg CreateContentVersionParams) error + DeleteAllSiteCollections(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 + DeleteOldCollectionItemVersions(ctx context.Context, arg DeleteOldCollectionItemVersionsParams) 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) GetAllVersionsForSite(ctx context.Context, arg GetAllVersionsForSiteParams) ([]GetAllVersionsForSiteRow, 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) GetContentVersion(ctx context.Context, versionID int64) (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 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) + UpsertCollection(ctx context.Context, arg UpsertCollectionParams) (Collection, error) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) } diff --git a/internal/db/sqlite/schema.sql b/internal/db/sqlite/schema.sql index f8a91f5..0b5d17b 100644 --- a/internal/db/sqlite/schema.sql +++ b/internal/db/sqlite/schema.sql @@ -22,15 +22,97 @@ CREATE TABLE content_versions ( 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 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_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 CREATE TRIGGER IF NOT EXISTS update_content_updated_at AFTER UPDATE ON content FOR EACH ROW BEGIN UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id; +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; \ No newline at end of file diff --git a/internal/db/sqlite/setup.sql b/internal/db/sqlite/setup.sql index df2a13c..c06978c 100644 --- a/internal/db/sqlite/setup.sql +++ b/internal/db/sqlite/setup.sql @@ -21,6 +21,59 @@ CREATE TABLE IF NOT EXISTS content_versions ( 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 CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id); @@ -30,10 +83,52 @@ CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at); -- name: CreateVersionsLookupIndex :exec 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 CREATE TRIGGER IF NOT EXISTS update_content_updated_at AFTER UPDATE ON content FOR EACH ROW BEGIN UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id; +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; \ No newline at end of file diff --git a/internal/db/sqlite/setup.sql.go b/internal/db/sqlite/setup.sql.go index 20df7d7..6599605 100644 --- a/internal/db/sqlite/setup.sql.go +++ b/internal/db/sqlite/setup.sql.go @@ -9,6 +9,83 @@ import ( "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 CREATE TABLE IF NOT EXISTS content ( id TEXT NOT NULL, diff --git a/internal/engine/database_client.go b/internal/engine/database_client.go index cb6cde3..9ec87ac 100644 --- a/internal/engine/database_client.go +++ b/internal/engine/database_client.go @@ -217,3 +217,196 @@ func toNullString(s string) sql.NullString { } 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()) + } +} diff --git a/internal/engine/engine.go b/internal/engine/engine.go index c78f349..0617a84 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -52,14 +52,14 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro return nil, fmt.Errorf("parsing HTML: %w", err) } - // 2. Find insertr elements - elements := e.findInsertrElements(doc) + // 2. Find insertr and collection elements + insertrElements, collectionElements := e.findEditableElements(doc) - // 3. Generate IDs for elements + // 3. Process regular .insertr elements 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) 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 { err = e.injectContent(processedElements, input.SiteID) 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 { injector := NewInjectorWithAuth(e.client, input.SiteID, e.authProvider) injector.InjectEditorAssets(doc, true, "") @@ -123,31 +144,40 @@ type InsertrElement struct { Node *html.Node } -// 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 { - var elements []InsertrElement +// CollectionElement represents an insertr-add collection element found in HTML +type CollectionElement struct { + Node *html.Node +} + +// findEditableElements finds all editable elements (.insertr and .insertr-add) +func (e *ContentEngine) findEditableElements(doc *html.Node) ([]InsertrElement, []CollectionElement) { + var insertrElements []InsertrElement + var collectionElements []CollectionElement 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) { - if n.Type == html.ElementNode && e.hasInsertrClass(n) { - if isContainer(n) { - // Container element - mark for transformation - containersToTransform = append(containersToTransform, n) - } else { - // Regular element - add directly - elements = append(elements, InsertrElement{ + if n.Type == html.ElementNode { + if e.hasInsertrClass(n) { + if isContainer(n) { + // Container element - mark for transformation + containersToTransform = append(containersToTransform, n) + } else { + // 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, }) } } }) - // 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 { // Remove .insertr class from container e.removeClass(container, "insertr") @@ -156,13 +186,23 @@ func (e *ContentEngine) findInsertrElements(doc *html.Node) []InsertrElement { viableChildren := FindViableChildren(container) for _, child := range viableChildren { e.addClass(child, "insertr") - elements = append(elements, InsertrElement{ + insertrElements = append(insertrElements, InsertrElement{ 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 @@ -184,6 +224,17 @@ func (e *ContentEngine) hasInsertrClass(node *html.Node) bool { 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 // HTML-first approach: no content-type attribute needed func (e *ContentEngine) addContentAttributes(node *html.Node, contentID string) { @@ -342,3 +393,124 @@ func (e *ContentEngine) extractOriginalTemplate(node *html.Node) 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", "
New item
", 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 +} diff --git a/internal/engine/types.go b/internal/engine/types.go index 3cff90f..1cc017c 100644 --- a/internal/engine/types.go +++ b/internal/engine/types.go @@ -48,6 +48,12 @@ type ContentClient interface { GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) GetAllContent(siteID string) (map[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 @@ -65,3 +71,39 @@ type ContentResponse struct { Content []ContentItem `json:"content"` 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"` +} diff --git a/lib/src/core/editor.js b/lib/src/core/editor.js index b7a56f5..c183e22 100644 --- a/lib/src/core/editor.js +++ b/lib/src/core/editor.js @@ -1,4 +1,5 @@ import { InsertrFormRenderer } from '../ui/form-renderer.js'; +import { CollectionManager } from '../ui/collection-manager.js'; /** * InsertrEditor - Content editing workflow and business logic @@ -67,8 +68,18 @@ export class InsertrEditor { initializeElement(meta) { const { element } = meta; - // Add click handler for editing - this.addClickHandler(element, meta); + if (meta.editorType === 'collection') { + // 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) { diff --git a/lib/src/core/insertr.js b/lib/src/core/insertr.js index 690e39d..b5d3393 100644 --- a/lib/src/core/insertr.js +++ b/lib/src/core/insertr.js @@ -12,17 +12,19 @@ export class InsertrCore { // Find all enhanced elements on the page findEnhancedElements() { - return document.querySelectorAll('.insertr'); + return document.querySelectorAll('.insertr, .insertr-add'); } // Get element metadata getElementMetadata(element) { const existingId = element.getAttribute('data-content-id'); + const isCollection = element.classList.contains('insertr-add'); return { contentId: existingId, element: element, - htmlMarkup: element.outerHTML // Server will generate ID from this + htmlMarkup: element.outerHTML, // Server will generate ID from this + editorType: isCollection ? 'collection' : 'element' }; } diff --git a/lib/src/styles/insertr.css b/lib/src/styles/insertr.css index 1fe0049..31e433e 100644 --- a/lib/src/styles/insertr.css +++ b/lib/src/styles/insertr.css @@ -827,4 +827,130 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after { margin-right: 0; 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; + } } \ No newline at end of file diff --git a/lib/src/ui/collection-manager.js b/lib/src/ui/collection-manager.js new file mode 100644 index 0000000..54929de --- /dev/null +++ b/lib/src/ui/collection-manager.js @@ -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'); + } +} \ No newline at end of file