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:
22
cmd/serve.go
22
cmd/serve.go
@@ -110,7 +110,7 @@ func runServe(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize content client for site manager
|
// Initialize content client for site manager
|
||||||
contentClient := content.NewDatabaseClient(database)
|
contentClient := engine.NewDatabaseClient(database)
|
||||||
|
|
||||||
// Initialize site manager with auth provider
|
// Initialize site manager with auth provider
|
||||||
authProvider := &engine.AuthProvider{Type: authConfig.Provider}
|
authProvider := &engine.AuthProvider{Type: authConfig.Provider}
|
||||||
@@ -227,6 +227,18 @@ func runServe(cmd *cobra.Command, args []string) {
|
|||||||
contentRouter.Get("/{id}/versions", contentHandler.GetContentVersions)
|
contentRouter.Get("/{id}/versions", contentHandler.GetContentVersions)
|
||||||
contentRouter.Post("/{id}/rollback", contentHandler.RollbackContent)
|
contentRouter.Post("/{id}/rollback", contentHandler.RollbackContent)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Collection endpoints
|
||||||
|
apiRouter.Route("/collections", func(collectionRouter chi.Router) {
|
||||||
|
collectionRouter.Get("/", contentHandler.GetAllCollections)
|
||||||
|
collectionRouter.Get("/{id}", contentHandler.GetCollection)
|
||||||
|
|
||||||
|
// Collection item endpoints
|
||||||
|
collectionRouter.Get("/{id}/items", contentHandler.GetCollectionItems)
|
||||||
|
collectionRouter.Post("/{id}/items", contentHandler.CreateCollectionItem)
|
||||||
|
collectionRouter.Put("/{id}/items/{item_id}", contentHandler.UpdateCollectionItem)
|
||||||
|
collectionRouter.Delete("/{id}/items/{item_id}", contentHandler.DeleteCollectionItem)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Static site serving - serve registered sites at /sites/{site_id}
|
// Static site serving - serve registered sites at /sites/{site_id}
|
||||||
@@ -259,6 +271,7 @@ func runServe(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Printf("🌐 Server running at: http://localhost%s\n", addr)
|
fmt.Printf("🌐 Server running at: http://localhost%s\n", addr)
|
||||||
fmt.Printf("💚 Health check: http://localhost%s/health\n", addr)
|
fmt.Printf("💚 Health check: http://localhost%s/health\n", addr)
|
||||||
fmt.Printf("📊 API endpoints:\n")
|
fmt.Printf("📊 API endpoints:\n")
|
||||||
|
fmt.Printf(" Content:\n")
|
||||||
fmt.Printf(" GET /api/content?site_id={site}\n")
|
fmt.Printf(" GET /api/content?site_id={site}\n")
|
||||||
fmt.Printf(" GET /api/content/{id}?site_id={site}\n")
|
fmt.Printf(" GET /api/content/{id}?site_id={site}\n")
|
||||||
fmt.Printf(" GET /api/content/bulk?site_id={site}&ids[]={id1}&ids[]={id2}\n")
|
fmt.Printf(" GET /api/content/bulk?site_id={site}&ids[]={id1}&ids[]={id2}\n")
|
||||||
@@ -266,6 +279,13 @@ func runServe(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Printf(" PUT /api/content/{id}\n")
|
fmt.Printf(" PUT /api/content/{id}\n")
|
||||||
fmt.Printf(" GET /api/content/{id}/versions?site_id={site}\n")
|
fmt.Printf(" GET /api/content/{id}/versions?site_id={site}\n")
|
||||||
fmt.Printf(" POST /api/content/{id}/rollback\n")
|
fmt.Printf(" POST /api/content/{id}/rollback\n")
|
||||||
|
fmt.Printf(" Collections:\n")
|
||||||
|
fmt.Printf(" GET /api/collections?site_id={site}\n")
|
||||||
|
fmt.Printf(" GET /api/collections/{id}?site_id={site}\n")
|
||||||
|
fmt.Printf(" GET /api/collections/{id}/items?site_id={site}\n")
|
||||||
|
fmt.Printf(" POST /api/collections/{id}/items\n")
|
||||||
|
fmt.Printf(" PUT /api/collections/{id}/items/{item_id}\n")
|
||||||
|
fmt.Printf(" DELETE /api/collections/{id}/items/{item_id}\n")
|
||||||
fmt.Printf("🌐 Static sites:\n")
|
fmt.Printf("🌐 Static sites:\n")
|
||||||
for siteID, _ := range siteManager.GetAllSites() {
|
for siteID, _ := range siteManager.GetAllSites() {
|
||||||
fmt.Printf(" %s: http://localhost%s/sites/%s/\n", siteID, addr, siteID)
|
fmt.Printf(" %s: http://localhost%s/sites/%s/\n", siteID, addr, siteID)
|
||||||
|
|||||||
@@ -858,3 +858,437 @@ func (h *ContentHandler) ServeInsertrCSS(w http.ResponseWriter, r *http.Request)
|
|||||||
// Copy file contents to response
|
// Copy file contents to response
|
||||||
io.Copy(w, file)
|
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"`
|
VersionID int64 `json:"version_id"`
|
||||||
RolledBackBy string `json:"rolled_back_by,omitempty"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,252 +0,0 @@
|
|||||||
package content
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/insertr/insertr/internal/db"
|
|
||||||
"github.com/insertr/insertr/internal/db/postgresql"
|
|
||||||
"github.com/insertr/insertr/internal/db/sqlite"
|
|
||||||
"github.com/insertr/insertr/internal/engine"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Helper function to convert sql.NullString to string
|
|
||||||
func getStringFromNullString(ns sql.NullString) string {
|
|
||||||
if ns.Valid {
|
|
||||||
return ns.String
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// DatabaseClient implements ContentClient for direct database access
|
|
||||||
type DatabaseClient struct {
|
|
||||||
db *db.Database
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDatabaseClient creates a new database content client
|
|
||||||
func NewDatabaseClient(database *db.Database) *DatabaseClient {
|
|
||||||
return &DatabaseClient{
|
|
||||||
db: database,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContent fetches a single content item by ID
|
|
||||||
func (d *DatabaseClient) GetContent(siteID, contentID string) (*engine.ContentItem, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
var content interface{}
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch d.db.GetDBType() {
|
|
||||||
case "sqlite3":
|
|
||||||
content, err = d.db.GetSQLiteQueries().GetContent(ctx, sqlite.GetContentParams{
|
|
||||||
ID: contentID,
|
|
||||||
SiteID: siteID,
|
|
||||||
})
|
|
||||||
case "postgresql":
|
|
||||||
content, err = d.db.GetPostgreSQLQueries().GetContent(ctx, postgresql.GetContentParams{
|
|
||||||
ID: contentID,
|
|
||||||
SiteID: siteID,
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported database type: %s", d.db.GetDBType())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, nil // Content not found, return nil without error
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("database error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
item := d.convertToContentItem(content)
|
|
||||||
return &item, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBulkContent fetches multiple content items by IDs
|
|
||||||
func (d *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map[string]engine.ContentItem, error) {
|
|
||||||
if len(contentIDs) == 0 {
|
|
||||||
return make(map[string]engine.ContentItem), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
var dbContent interface{}
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch d.db.GetDBType() {
|
|
||||||
case "sqlite3":
|
|
||||||
dbContent, err = d.db.GetSQLiteQueries().GetBulkContent(ctx, sqlite.GetBulkContentParams{
|
|
||||||
SiteID: siteID,
|
|
||||||
Ids: contentIDs,
|
|
||||||
})
|
|
||||||
case "postgresql":
|
|
||||||
dbContent, err = d.db.GetPostgreSQLQueries().GetBulkContent(ctx, postgresql.GetBulkContentParams{
|
|
||||||
SiteID: siteID,
|
|
||||||
Ids: contentIDs,
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported database type: %s", d.db.GetDBType())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("database error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
items := d.convertToContentItemList(dbContent)
|
|
||||||
|
|
||||||
// Convert slice to map for easy lookup
|
|
||||||
result := make(map[string]engine.ContentItem)
|
|
||||||
for _, item := range items {
|
|
||||||
result[item.ID] = item
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllContent fetches all content for a site
|
|
||||||
func (d *DatabaseClient) GetAllContent(siteID string) (map[string]engine.ContentItem, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
var dbContent interface{}
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch d.db.GetDBType() {
|
|
||||||
case "sqlite3":
|
|
||||||
dbContent, err = d.db.GetSQLiteQueries().GetAllContent(ctx, siteID)
|
|
||||||
case "postgresql":
|
|
||||||
dbContent, err = d.db.GetPostgreSQLQueries().GetAllContent(ctx, siteID)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported database type: %s", d.db.GetDBType())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("database error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
items := d.convertToContentItemList(dbContent)
|
|
||||||
|
|
||||||
// Convert slice to map for easy lookup
|
|
||||||
result := make(map[string]engine.ContentItem)
|
|
||||||
for _, item := range items {
|
|
||||||
result[item.ID] = item
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertToContentItem converts database models to engine.ContentItem
|
|
||||||
func (d *DatabaseClient) convertToContentItem(content interface{}) engine.ContentItem {
|
|
||||||
switch d.db.GetDBType() {
|
|
||||||
case "sqlite3":
|
|
||||||
c := content.(sqlite.Content)
|
|
||||||
return engine.ContentItem{
|
|
||||||
ID: c.ID,
|
|
||||||
SiteID: c.SiteID,
|
|
||||||
HTMLContent: c.HtmlContent,
|
|
||||||
OriginalTemplate: getStringFromNullString(c.OriginalTemplate),
|
|
||||||
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
case "postgresql":
|
|
||||||
c := content.(postgresql.Content)
|
|
||||||
return engine.ContentItem{
|
|
||||||
ID: c.ID,
|
|
||||||
SiteID: c.SiteID,
|
|
||||||
HTMLContent: c.HtmlContent,
|
|
||||||
OriginalTemplate: getStringFromNullString(c.OriginalTemplate),
|
|
||||||
UpdatedAt: time.Unix(c.UpdatedAt, 0).Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return engine.ContentItem{} // Should never happen
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertToContentItemList converts database model lists to engine.ContentItem slice
|
|
||||||
func (d *DatabaseClient) convertToContentItemList(contentList interface{}) []engine.ContentItem {
|
|
||||||
switch d.db.GetDBType() {
|
|
||||||
case "sqlite3":
|
|
||||||
list := contentList.([]sqlite.Content)
|
|
||||||
items := make([]engine.ContentItem, len(list))
|
|
||||||
for i, content := range list {
|
|
||||||
items[i] = d.convertToContentItem(content)
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
case "postgresql":
|
|
||||||
list := contentList.([]postgresql.Content)
|
|
||||||
items := make([]engine.ContentItem, len(list))
|
|
||||||
for i, content := range list {
|
|
||||||
items[i] = d.convertToContentItem(content)
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
return []engine.ContentItem{} // Should never happen
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateContent creates a new content item
|
|
||||||
func (c *DatabaseClient) CreateContent(siteID, contentID, htmlContent, originalTemplate, lastEditedBy string) (*engine.ContentItem, error) {
|
|
||||||
switch c.db.GetDBType() {
|
|
||||||
case "sqlite3":
|
|
||||||
content, err := c.db.GetSQLiteQueries().CreateContent(context.Background(), sqlite.CreateContentParams{
|
|
||||||
ID: contentID,
|
|
||||||
SiteID: siteID,
|
|
||||||
HtmlContent: htmlContent,
|
|
||||||
OriginalTemplate: toNullString(originalTemplate),
|
|
||||||
LastEditedBy: lastEditedBy,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &engine.ContentItem{
|
|
||||||
ID: content.ID,
|
|
||||||
SiteID: content.SiteID,
|
|
||||||
HTMLContent: content.HtmlContent,
|
|
||||||
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
|
||||||
LastEditedBy: content.LastEditedBy,
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
case "postgresql":
|
|
||||||
content, err := c.db.GetPostgreSQLQueries().CreateContent(context.Background(), postgresql.CreateContentParams{
|
|
||||||
ID: contentID,
|
|
||||||
SiteID: siteID,
|
|
||||||
HtmlContent: htmlContent,
|
|
||||||
OriginalTemplate: toNullString(originalTemplate),
|
|
||||||
LastEditedBy: lastEditedBy,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &engine.ContentItem{
|
|
||||||
ID: content.ID,
|
|
||||||
SiteID: content.SiteID,
|
|
||||||
HTMLContent: content.HtmlContent,
|
|
||||||
OriginalTemplate: getStringFromNullString(content.OriginalTemplate),
|
|
||||||
LastEditedBy: content.LastEditedBy,
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported database type: %s", c.db.GetDBType())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to convert string to sql.NullString
|
|
||||||
func toNullString(s string) sql.NullString {
|
|
||||||
if s == "" {
|
|
||||||
return sql.NullString{Valid: false}
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user