Implement complete collection REST API and fix critical server enhancement bug

• Add full collection REST API with CRUD operations for collections and items
• Implement collection models, handlers, and database integration with SQLite/PostgreSQL support
• Add collection endpoints: GET/POST/PUT/DELETE for collections and collection items
• Fix critical server enhancement bug by consolidating to engine.DatabaseClient
• Remove obsolete content.DatabaseClient that lacked collection support
• Enable proper collection reconstruction during server-side enhancement
• Collections now persist correctly and display new items after API modifications
This commit is contained in:
2025-09-22 20:12:34 +02:00
parent 2315ba4750
commit 09823d3e4d
4 changed files with 531 additions and 260 deletions

View File

@@ -858,3 +858,437 @@ func (h *ContentHandler) ServeInsertrCSS(w http.ResponseWriter, r *http.Request)
// Copy file contents to response
io.Copy(w, file)
}
// Collection API handlers
// GetCollection handles GET /api/collections/{id}
func (h *ContentHandler) GetCollection(w http.ResponseWriter, r *http.Request) {
collectionID := chi.URLParam(r, "id")
siteID := r.URL.Query().Get("site_id")
if siteID == "" {
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
return
}
var collection interface{}
var err error
switch h.database.GetDBType() {
case "sqlite3":
collection, err = h.database.GetSQLiteQueries().GetCollection(context.Background(), sqlite.GetCollectionParams{
ID: collectionID,
SiteID: siteID,
})
case "postgresql":
collection, err = h.database.GetPostgreSQLQueries().GetCollection(context.Background(), postgresql.GetCollectionParams{
ID: collectionID,
SiteID: siteID,
})
default:
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
return
}
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Collection not found", http.StatusNotFound)
return
}
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
return
}
apiCollection := h.convertToAPICollection(collection)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(apiCollection)
}
// GetAllCollections handles GET /api/collections
func (h *ContentHandler) GetAllCollections(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site_id")
if siteID == "" {
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
return
}
var collections interface{}
var err error
switch h.database.GetDBType() {
case "sqlite3":
collections, err = h.database.GetSQLiteQueries().GetAllCollections(context.Background(), siteID)
case "postgresql":
collections, err = h.database.GetPostgreSQLQueries().GetAllCollections(context.Background(), siteID)
default:
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
return
}
if err != nil {
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
return
}
apiCollections := h.convertToAPICollectionList(collections)
response := CollectionResponse{Collections: apiCollections}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// GetCollectionItems handles GET /api/collections/{id}/items
func (h *ContentHandler) GetCollectionItems(w http.ResponseWriter, r *http.Request) {
collectionID := chi.URLParam(r, "id")
siteID := r.URL.Query().Get("site_id")
if siteID == "" {
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
return
}
var items interface{}
var err error
switch h.database.GetDBType() {
case "sqlite3":
items, err = h.database.GetSQLiteQueries().GetCollectionItemsWithTemplate(context.Background(), sqlite.GetCollectionItemsWithTemplateParams{
CollectionID: collectionID,
SiteID: siteID,
})
case "postgresql":
items, err = h.database.GetPostgreSQLQueries().GetCollectionItemsWithTemplate(context.Background(), postgresql.GetCollectionItemsWithTemplateParams{
CollectionID: collectionID,
SiteID: siteID,
})
default:
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
return
}
if err != nil {
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
return
}
apiItems := h.convertToAPICollectionItemList(items)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"items": apiItems,
})
}
// CreateCollectionItem handles POST /api/collections/{id}/items
func (h *ContentHandler) CreateCollectionItem(w http.ResponseWriter, r *http.Request) {
collectionID := chi.URLParam(r, "id")
var req CreateCollectionItemRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Set defaults
if req.SiteID == "" {
req.SiteID = r.URL.Query().Get("site_id")
}
if req.SiteID == "" {
http.Error(w, "site_id is required", http.StatusBadRequest)
return
}
if req.CreatedBy == "" {
req.CreatedBy = "api"
}
if req.CollectionID == "" {
req.CollectionID = collectionID
}
// Generate item ID
itemID := fmt.Sprintf("%s-item-%d", collectionID, time.Now().Unix())
var createdItem interface{}
var err error
switch h.database.GetDBType() {
case "sqlite3":
createdItem, err = h.database.GetSQLiteQueries().CreateCollectionItem(context.Background(), sqlite.CreateCollectionItemParams{
ItemID: itemID,
CollectionID: req.CollectionID,
SiteID: req.SiteID,
TemplateID: int64(req.TemplateID),
HtmlContent: req.HTMLContent,
Position: int64(req.Position),
LastEditedBy: req.CreatedBy,
})
case "postgresql":
createdItem, err = h.database.GetPostgreSQLQueries().CreateCollectionItem(context.Background(), postgresql.CreateCollectionItemParams{
ItemID: itemID,
CollectionID: req.CollectionID,
SiteID: req.SiteID,
TemplateID: int32(req.TemplateID),
HtmlContent: req.HTMLContent,
Position: int32(req.Position),
LastEditedBy: req.CreatedBy,
})
default:
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
return
}
if err != nil {
http.Error(w, fmt.Sprintf("Failed to create collection item: %v", err), http.StatusInternalServerError)
return
}
apiItem := h.convertToAPICollectionItem(createdItem)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(apiItem)
}
// UpdateCollectionItem handles PUT /api/collections/{id}/items/{item_id}
func (h *ContentHandler) UpdateCollectionItem(w http.ResponseWriter, r *http.Request) {
collectionID := chi.URLParam(r, "id")
itemID := chi.URLParam(r, "item_id")
siteID := r.URL.Query().Get("site_id")
if siteID == "" {
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
return
}
var req UpdateCollectionItemRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if req.UpdatedBy == "" {
req.UpdatedBy = "api"
}
var updatedItem interface{}
var err error
switch h.database.GetDBType() {
case "sqlite3":
updatedItem, err = h.database.GetSQLiteQueries().UpdateCollectionItem(context.Background(), sqlite.UpdateCollectionItemParams{
ItemID: itemID,
CollectionID: collectionID,
SiteID: siteID,
HtmlContent: req.HTMLContent,
LastEditedBy: req.UpdatedBy,
})
case "postgresql":
updatedItem, err = h.database.GetPostgreSQLQueries().UpdateCollectionItem(context.Background(), postgresql.UpdateCollectionItemParams{
ItemID: itemID,
CollectionID: collectionID,
SiteID: siteID,
HtmlContent: req.HTMLContent,
LastEditedBy: req.UpdatedBy,
})
default:
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
return
}
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Collection item not found", http.StatusNotFound)
return
}
http.Error(w, fmt.Sprintf("Failed to update collection item: %v", err), http.StatusInternalServerError)
return
}
apiItem := h.convertToAPICollectionItem(updatedItem)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(apiItem)
}
// DeleteCollectionItem handles DELETE /api/collections/{id}/items/{item_id}
func (h *ContentHandler) DeleteCollectionItem(w http.ResponseWriter, r *http.Request) {
collectionID := chi.URLParam(r, "id")
itemID := chi.URLParam(r, "item_id")
siteID := r.URL.Query().Get("site_id")
if siteID == "" {
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
return
}
var err error
switch h.database.GetDBType() {
case "sqlite3":
err = h.database.GetSQLiteQueries().DeleteCollectionItem(context.Background(), sqlite.DeleteCollectionItemParams{
ItemID: itemID,
CollectionID: collectionID,
SiteID: siteID,
})
case "postgresql":
err = h.database.GetPostgreSQLQueries().DeleteCollectionItem(context.Background(), postgresql.DeleteCollectionItemParams{
ItemID: itemID,
CollectionID: collectionID,
SiteID: siteID,
})
default:
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
return
}
if err != nil {
http.Error(w, fmt.Sprintf("Failed to delete collection item: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
// Collection conversion helpers
// convertToAPICollection converts database collection to API model
func (h *ContentHandler) convertToAPICollection(dbCollection interface{}) CollectionItem {
switch h.database.GetDBType() {
case "sqlite3":
collection := dbCollection.(sqlite.Collection)
return CollectionItem{
ID: collection.ID,
SiteID: collection.SiteID,
ContainerHTML: collection.ContainerHtml,
CreatedAt: time.Unix(collection.CreatedAt, 0),
UpdatedAt: time.Unix(collection.UpdatedAt, 0),
LastEditedBy: collection.LastEditedBy,
}
case "postgresql":
collection := dbCollection.(postgresql.Collection)
return CollectionItem{
ID: collection.ID,
SiteID: collection.SiteID,
ContainerHTML: collection.ContainerHtml,
CreatedAt: time.Unix(collection.CreatedAt, 0),
UpdatedAt: time.Unix(collection.UpdatedAt, 0),
LastEditedBy: collection.LastEditedBy,
}
default:
return CollectionItem{}
}
}
// convertToAPICollectionList converts database collection list to API models
func (h *ContentHandler) convertToAPICollectionList(dbCollections interface{}) []CollectionItem {
switch h.database.GetDBType() {
case "sqlite3":
collections := dbCollections.([]sqlite.Collection)
result := make([]CollectionItem, len(collections))
for i, collection := range collections {
result[i] = CollectionItem{
ID: collection.ID,
SiteID: collection.SiteID,
ContainerHTML: collection.ContainerHtml,
CreatedAt: time.Unix(collection.CreatedAt, 0),
UpdatedAt: time.Unix(collection.UpdatedAt, 0),
LastEditedBy: collection.LastEditedBy,
}
}
return result
case "postgresql":
collections := dbCollections.([]postgresql.Collection)
result := make([]CollectionItem, len(collections))
for i, collection := range collections {
result[i] = CollectionItem{
ID: collection.ID,
SiteID: collection.SiteID,
ContainerHTML: collection.ContainerHtml,
CreatedAt: time.Unix(collection.CreatedAt, 0),
UpdatedAt: time.Unix(collection.UpdatedAt, 0),
LastEditedBy: collection.LastEditedBy,
}
}
return result
default:
return []CollectionItem{}
}
}
// convertToAPICollectionItem converts database collection item to API model
func (h *ContentHandler) convertToAPICollectionItem(dbItem interface{}) CollectionItemData {
switch h.database.GetDBType() {
case "sqlite3":
item := dbItem.(sqlite.CollectionItem)
return CollectionItemData{
ItemID: item.ItemID,
CollectionID: item.CollectionID,
SiteID: item.SiteID,
TemplateID: int(item.TemplateID),
HTMLContent: item.HtmlContent,
Position: int(item.Position),
CreatedAt: time.Unix(item.CreatedAt, 0),
UpdatedAt: time.Unix(item.UpdatedAt, 0),
LastEditedBy: item.LastEditedBy,
}
case "postgresql":
item := dbItem.(postgresql.CollectionItem)
return CollectionItemData{
ItemID: item.ItemID,
CollectionID: item.CollectionID,
SiteID: item.SiteID,
TemplateID: int(item.TemplateID),
HTMLContent: item.HtmlContent,
Position: int(item.Position),
CreatedAt: time.Unix(item.CreatedAt, 0),
UpdatedAt: time.Unix(item.UpdatedAt, 0),
LastEditedBy: item.LastEditedBy,
}
default:
return CollectionItemData{}
}
}
// convertToAPICollectionItemList converts database collection item list to API models
func (h *ContentHandler) convertToAPICollectionItemList(dbItems interface{}) []CollectionItemData {
switch h.database.GetDBType() {
case "sqlite3":
items := dbItems.([]sqlite.GetCollectionItemsWithTemplateRow)
result := make([]CollectionItemData, len(items))
for i, item := range items {
result[i] = CollectionItemData{
ItemID: item.ItemID,
CollectionID: item.CollectionID,
SiteID: item.SiteID,
TemplateID: int(item.TemplateID),
HTMLContent: item.HtmlContent,
Position: int(item.Position),
CreatedAt: time.Unix(item.CreatedAt, 0),
UpdatedAt: time.Unix(item.UpdatedAt, 0),
LastEditedBy: item.LastEditedBy,
TemplateName: item.TemplateName,
}
}
return result
case "postgresql":
items := dbItems.([]postgresql.GetCollectionItemsWithTemplateRow)
result := make([]CollectionItemData, len(items))
for i, item := range items {
result[i] = CollectionItemData{
ItemID: item.ItemID,
CollectionID: item.CollectionID,
SiteID: item.SiteID,
TemplateID: int(item.TemplateID),
HTMLContent: item.HtmlContent,
Position: int(item.Position),
CreatedAt: time.Unix(item.CreatedAt, 0),
UpdatedAt: time.Unix(item.UpdatedAt, 0),
LastEditedBy: item.LastEditedBy,
TemplateName: item.TemplateName,
}
}
return result
default:
return []CollectionItemData{}
}
}