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:
@@ -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{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,3 +54,72 @@ type RollbackContentRequest struct {
|
||||
VersionID int64 `json:"version_id"`
|
||||
RolledBackBy string `json:"rolled_back_by,omitempty"`
|
||||
}
|
||||
|
||||
// Collection API models
|
||||
type CollectionItem struct {
|
||||
ID string `json:"id"`
|
||||
SiteID string `json:"site_id"`
|
||||
ContainerHTML string `json:"container_html"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
LastEditedBy string `json:"last_edited_by"`
|
||||
Templates []CollectionTemplate `json:"templates,omitempty"`
|
||||
Items []CollectionItemData `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
type CollectionTemplate 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"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type CollectionItemData 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"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
LastEditedBy string `json:"last_edited_by"`
|
||||
TemplateName string `json:"template_name,omitempty"`
|
||||
}
|
||||
|
||||
type CollectionResponse struct {
|
||||
Collections []CollectionItem `json:"collections"`
|
||||
}
|
||||
|
||||
// Collection request models
|
||||
type CreateCollectionRequest struct {
|
||||
ContainerHTML string `json:"container_html"`
|
||||
SiteID string `json:"site_id,omitempty"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
}
|
||||
|
||||
type CreateCollectionTemplateRequest struct {
|
||||
Name string `json:"name"`
|
||||
HTMLTemplate string `json:"html_template"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
CollectionID string `json:"collection_id,omitempty"`
|
||||
SiteID string `json:"site_id,omitempty"`
|
||||
}
|
||||
|
||||
type CreateCollectionItemRequest struct {
|
||||
TemplateID int `json:"template_id"`
|
||||
HTMLContent string `json:"html_content"`
|
||||
Position int `json:"position"`
|
||||
CollectionID string `json:"collection_id,omitempty"`
|
||||
SiteID string `json:"site_id,omitempty"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateCollectionItemRequest struct {
|
||||
HTMLContent string `json:"html_content"`
|
||||
Position int `json:"position"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user