feat: implement single POST upsert API with automatic ID generation
- Replace separate POST/PUT endpoints with unified POST upsert - Add automatic content ID generation from element context when no ID provided - Implement version history preservation before content updates - Add element context support for backend ID generation - Update frontend to use single endpoint for all content operations - Enhanced demo site with latest database content including proper content IDs
This commit is contained in:
@@ -130,7 +130,7 @@ func runServe(cmd *cobra.Command, args []string) {
|
||||
contentRouter := apiRouter.PathPrefix("/content").Subrouter()
|
||||
contentRouter.HandleFunc("/bulk", contentHandler.GetBulkContent).Methods("GET")
|
||||
contentRouter.HandleFunc("/{id}", contentHandler.GetContent).Methods("GET")
|
||||
contentRouter.HandleFunc("/{id}", contentHandler.UpdateContent).Methods("PUT")
|
||||
|
||||
contentRouter.HandleFunc("", contentHandler.GetAllContent).Methods("GET")
|
||||
contentRouter.HandleFunc("", contentHandler.CreateContent).Methods("POST")
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<h1 class="logo insertr" >Acme Consulting</h1>
|
||||
<h1 class="logo insertr" data-content-id="navbar-logo-2b10ad" data-content-type="text">Acme Consulting</h1>
|
||||
<ul class="nav-links">
|
||||
<li><a href="index.html">Home</a></li>
|
||||
<li><a href="about.html">About</a></li>
|
||||
@@ -22,15 +22,15 @@
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1 class="insertr" >About Acme Consulting</h1>
|
||||
<p class="lead insertr" >We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace.</p>
|
||||
<h1 class="insertr" data-content-id="hero-title-c70343" data-content-type="text">About Acme Consulting</h1>
|
||||
<p class="lead insertr" data-content-id="hero-lead-673026" data-content-type="markdown">We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Story Section -->
|
||||
<section class="services">
|
||||
<div class="container">
|
||||
<h2 class="insertr" >Our Story</h2>
|
||||
<h2 class="insertr" data-content-id="services-subtitle-c8927c" data-content-type="text">Our Story</h2>
|
||||
<div class="insertr-group">
|
||||
<p>Founded in 2020, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.</p>
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
<!-- Team Section -->
|
||||
<section class="cta">
|
||||
<div class="container">
|
||||
<h2 class="insertr" >Our Team</h2>
|
||||
<p class="insertr" >We're a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p>
|
||||
<h2 class="insertr" data-content-id="subtitle-ba6444" data-content-type="text">Our Team</h2>
|
||||
<p class="insertr" data-content-id="text-8b3502" data-content-type="markdown">We're a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p>
|
||||
|
||||
<div class="services-grid" style="margin-top: 3rem;">
|
||||
<div class="service-card">
|
||||
@@ -76,19 +76,19 @@
|
||||
<!-- Values Section -->
|
||||
<section class="testimonial">
|
||||
<div class="container">
|
||||
<h2 class="insertr" style="margin-bottom: 2rem;" >Our Values</h2>
|
||||
<h2 class="insertr" style="margin-bottom: 2rem;" data-content-id="testimonial-subtitle-dce35b" data-content-type="text">Our Values</h2>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; text-align: left;">
|
||||
<div>
|
||||
<h3 class="insertr" >Client-First</h3>
|
||||
<p class="insertr" >Every recommendation we make is designed with your specific business context and goals in mind.</p>
|
||||
<h3 class="insertr" data-content-id="testimonial-heading-f7eefa" data-content-type="text">Client-First</h3>
|
||||
<p class="insertr" data-content-id="testimonial-text-f7e46f" data-content-type="markdown">Every recommendation we make is designed with your specific business context and goals in mind.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="insertr" >Practical Solutions</h3>
|
||||
<p class="insertr" >We believe in strategies that you can actually implement with your current resources and capabilities.</p>
|
||||
<h3 class="insertr" data-content-id="testimonial-heading-fd4293" data-content-type="text">Practical Solutions</h3>
|
||||
<p class="insertr" data-content-id="testimonial-text-c12023" data-content-type="markdown">We believe in strategies that you can actually implement with your current resources and capabilities.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="insertr" >Long-term Partnership</h3>
|
||||
<p class="insertr" >We're not just consultants; we're partners in your business success for the long haul.</p>
|
||||
<h3 class="insertr" data-content-id="testimonial-heading-957cc8" data-content-type="text">Long-term Partnership</h3>
|
||||
<p class="insertr" data-content-id="testimonial-text-45dae2" data-content-type="markdown">We're not just consultants; we're partners in your business success for the long haul.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,7 +97,7 @@
|
||||
<!-- Test Section for Insertr Features -->
|
||||
<section class="testimonial">
|
||||
<div class="container">
|
||||
<h2 class="insertr" >Feature Tests</h2>
|
||||
<h2 class="insertr" data-content-id="testimonial-subtitle-648b88" data-content-type="text">Feature Tests</h2>
|
||||
|
||||
<!-- Test 1: .insertr container expansion (should make each p individually editable) -->
|
||||
<div style="margin-bottom: 2rem;">
|
||||
@@ -124,8 +124,8 @@
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p class="insertr" >© 2024 Acme Consulting Services. All rights reserved.</p>
|
||||
<p class="insertr" >📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <button class="insertr-gate" style="background: none; border: 1px solid #ccc; padding: 4px 8px; margin-left: 10px; border-radius: 3px; font-size: 11px;">🔧 Edit</button></p>
|
||||
<p class="insertr" data-content-id="footer-text-a2b6a8" data-content-type="markdown">© 2024 Acme Consulting Services. All rights reserved.</p>
|
||||
<p class="insertr" data-content-id="footer-text-a44170" data-content-type="markdown">📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <button class="insertr-gate" style="background: none; border: 1px solid #ccc; padding: 4px 8px; margin-left: 10px; border-radius: 3px; font-size: 11px;">🔧 Edit</button></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<h1 class="logo insertr" >Acme Consulting!!!</h1>
|
||||
<h1 class="logo insertr" data-content-id="navbar-logo-200530" data-content-type="text">Acme Consulting</h1>
|
||||
<ul class="nav-links">
|
||||
<li><a href="index.html">Home</a></li>
|
||||
<li><a href="about.html">About</a></li>
|
||||
@@ -22,30 +22,30 @@
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1 class="insertr" >!Transform Your Business with Expert Consulting!</h1>
|
||||
<p class="lead insertr" >We help small businesses grow through strategic planning, process optimization, and digital transformation. Our team brings 15+ years of experience to drive your success. Superb</p>
|
||||
<a href="contact.html" class="btn-primary insertr" >Get Started Today?</a>
|
||||
<h1 class="insertr" data-content-id="hero-title-a1de7b" data-content-type="text">Transform Your Business with Expert Consulting!</h1>
|
||||
<p class="lead insertr" data-content-id="hero-lead-abb35f" data-content-type="markdown">We help small businesses **grow** through strategic planning, process optimization, and digital transformation. Our team brings 15+ years of experience to drive your success.</p>
|
||||
<a href="contact.html" class="btn-primary insertr" data-content-id="hero-link-20f13f" data-content-type="link">Get Started Today</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Services Section -->
|
||||
<section class="services">
|
||||
<div class="container">
|
||||
<h2 class="insertr" >Our Services</h2>
|
||||
<p class="section-subtitle insertr" >Comprehensive solutions tailored to your business needs</p>
|
||||
<h2 class="insertr" data-content-id="services-subtitle-7a7725" data-content-type="text">Our Offers</h2>
|
||||
<p class="section-subtitle insertr" data-content-id="services-title-66a36e" data-content-type="markdown">Comprehensive solutions tailored to your business needs</p>
|
||||
|
||||
<div class="services-grid">
|
||||
<div class="service-card">
|
||||
<h3 class="insertr" >Strategic Planning</h3>
|
||||
<p class="insertr" >Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.</p>
|
||||
<h3 class="insertr" data-content-id="services-heading-6dd5f2" data-content-type="text">Strategic Planning</h3>
|
||||
<p class="insertr" data-content-id="services-text-3a002f" data-content-type="markdown">Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.</p>
|
||||
</div>
|
||||
<div class="service-card">
|
||||
<h3 class="insertr" >Operations Optimization</h3>
|
||||
<p class="insertr" >Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.</p>
|
||||
<h3 class="insertr" data-content-id="services-heading-fdebeb" data-content-type="text">Operations Optimization</h3>
|
||||
<p class="insertr" data-content-id="services-text-31ddbd" data-content-type="markdown">Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.</p>
|
||||
</div>
|
||||
<div class="service-card">
|
||||
<h3 class="insertr" >Digital Transformation</h3>
|
||||
<p class="insertr" >Modernize your technology stack and digital presence to compete effectively in today's marketplace.</p>
|
||||
<h3 class="insertr" data-content-id="services-heading-0d6ef9" data-content-type="text">Digital Transformation</h3>
|
||||
<p class="insertr" data-content-id="services-text-bd1837" data-content-type="markdown">Modernize your technology stack and digital presence to compete effectively in today's marketplace.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,8 +55,8 @@
|
||||
<section class="testimonial">
|
||||
<div class="container">
|
||||
<blockquote>
|
||||
<p class="insertr" >"Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations."</p>
|
||||
<cite class="insertr" >Sarah Johnson, CEO of TechStart Inc.</cite>
|
||||
<p class="insertr" data-content-id="testimonial-text-69de1a" data-content-type="markdown">"Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations."</p>
|
||||
<cite class="insertr" data-content-id="testimonial-content-dfd023" data-content-type="text">Sarah Johnson, CEO of TechStart Inc.</cite>
|
||||
</blockquote>
|
||||
</div>
|
||||
</section>
|
||||
@@ -64,17 +64,17 @@
|
||||
<!-- Call to Action -->
|
||||
<section class="cta">
|
||||
<div class="container">
|
||||
<h2 class="insertr" >Ready to Transform Your Business?</h2>
|
||||
<p class="insertr" >Contact us today for a free consultation and discover how we can help you achieve your goals.</p>
|
||||
<a href="contact.html" class="btn-primary insertr" >Schedule Consultation</a>
|
||||
<h2 class="insertr" data-content-id="subtitle-d0ebd3" data-content-type="text">Ready to Transform Your Business?</h2>
|
||||
<p class="insertr" data-content-id="text-a588c5" data-content-type="markdown">Contact us today for a free consultation and discover how we can help you achieve your goals.</p>
|
||||
<a href="contact.html" class="btn-primary insertr" data-content-id="link-d11ae9" data-content-type="link">Schedule Consultation</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p class="insertr" >© 2024 Acme Consulting Services. All rights reserved.</p>
|
||||
<p class="insertr" >📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <a href="#" class="insertr-gate">Editor</a></p>
|
||||
<p class="insertr" data-content-id="footer-text-a2b6a8-22fbf8" data-content-type="markdown">© 2024 Acme Consulting Services. All rights reserved.</p>
|
||||
<p class="insertr" data-content-id="footer-text-d73f32" data-content-type="markdown">📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <a href="#" class="insertr-gate">Editor</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -252,68 +252,6 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
userID := userInfo.ID
|
||||
|
||||
var content interface{}
|
||||
var err error
|
||||
|
||||
switch h.database.GetDBType() {
|
||||
case "sqlite3":
|
||||
content, err = h.database.GetSQLiteQueries().CreateContent(context.Background(), sqlite.CreateContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
Value: req.Value,
|
||||
Type: req.Type,
|
||||
LastEditedBy: userID,
|
||||
})
|
||||
case "postgresql":
|
||||
content, err = h.database.GetPostgreSQLQueries().CreateContent(context.Background(), postgresql.CreateContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
Value: req.Value,
|
||||
Type: req.Type,
|
||||
LastEditedBy: userID,
|
||||
})
|
||||
default:
|
||||
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to create content: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
item := h.convertToAPIContent(content)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(item)
|
||||
}
|
||||
|
||||
// UpdateContent handles PUT /api/content/{id} with upsert functionality
|
||||
func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
contentID := vars["id"]
|
||||
siteID := r.URL.Query().Get("site_id")
|
||||
|
||||
if siteID == "" {
|
||||
http.Error(w, "site_id parameter is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateContentRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract user from request using authentication service
|
||||
userInfo, authErr := h.authService.ExtractUserFromRequest(r)
|
||||
if authErr != nil {
|
||||
http.Error(w, fmt.Sprintf("Authentication error: %v", authErr), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
userID := userInfo.ID
|
||||
|
||||
// Check if content exists for version history (non-blocking)
|
||||
var existingContent interface{}
|
||||
var contentExists bool
|
||||
@@ -350,13 +288,12 @@ func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
|
||||
contentType = "text" // default type for new content
|
||||
}
|
||||
|
||||
// Perform upsert operation
|
||||
var upsertedContent interface{}
|
||||
var content interface{}
|
||||
var err error
|
||||
|
||||
switch h.database.GetDBType() {
|
||||
case "sqlite3":
|
||||
upsertedContent, err = h.database.GetSQLiteQueries().UpsertContent(context.Background(), sqlite.UpsertContentParams{
|
||||
content, err = h.database.GetSQLiteQueries().UpsertContent(context.Background(), sqlite.UpsertContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
Value: req.Value,
|
||||
@@ -364,7 +301,7 @@ func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
|
||||
LastEditedBy: userID,
|
||||
})
|
||||
case "postgresql":
|
||||
upsertedContent, err = h.database.GetPostgreSQLQueries().UpsertContent(context.Background(), postgresql.UpsertContentParams{
|
||||
content, err = h.database.GetPostgreSQLQueries().UpsertContent(context.Background(), postgresql.UpsertContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
Value: req.Value,
|
||||
@@ -381,7 +318,7 @@ func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
item := h.convertToAPIContent(upsertedContent)
|
||||
item := h.convertToAPIContent(content)
|
||||
|
||||
// Trigger file enhancement if site is registered for auto-enhancement
|
||||
if h.siteManager != nil && h.siteManager.IsAutoEnhanceEnabled(siteID) {
|
||||
@@ -395,6 +332,7 @@ func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(item)
|
||||
}
|
||||
|
||||
|
||||
@@ -50,12 +50,6 @@ type CreateContentRequest struct {
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateContentRequest struct {
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
}
|
||||
|
||||
type RollbackContentRequest struct {
|
||||
VersionID int64 `json:"version_id"`
|
||||
RolledBackBy string `json:"rolled_back_by,omitempty"`
|
||||
|
||||
@@ -28,36 +28,7 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
async updateContent(contentId, content) {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/${contentId}?site_id=${this.siteId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.getAuthToken()}`
|
||||
},
|
||||
body: JSON.stringify({ value: content })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log(`✅ Content updated: ${contentId}`);
|
||||
return true;
|
||||
} else {
|
||||
console.warn(`⚠️ Update failed (${response.status}): ${contentId}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
// Provide helpful error message for common development issues
|
||||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||
console.warn(`🔌 API Server not reachable at ${this.baseUrl}`);
|
||||
console.warn('💡 Start full-stack development: just dev');
|
||||
} else {
|
||||
console.error('Failed to update content:', contentId, error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async createContent(contentId, content, type, elementContext = null) {
|
||||
try {
|
||||
const payload = {
|
||||
|
||||
@@ -102,27 +102,22 @@ export class InsertrEditor {
|
||||
contentValue = formData.text || formData;
|
||||
}
|
||||
|
||||
if (meta.hasExistingId) {
|
||||
// Enhanced site - update existing content
|
||||
const updateSuccess = await this.apiClient.updateContent(meta.contentId, contentValue);
|
||||
if (!updateSuccess) {
|
||||
console.error('❌ Failed to update content:', meta.contentId);
|
||||
} else {
|
||||
console.log(`✅ Content updated:`, meta.contentId, contentValue);
|
||||
}
|
||||
// Universal upsert - works for both new and existing content
|
||||
const contentType = this.determineContentType(meta.element);
|
||||
const result = await this.apiClient.createContent(
|
||||
meta.contentId, // Use existing ID if available, null if new
|
||||
contentValue,
|
||||
contentType,
|
||||
meta.elementContext
|
||||
);
|
||||
|
||||
if (result) {
|
||||
// Store the backend-generated/confirmed ID in the element
|
||||
meta.element.setAttribute('data-content-id', result.id);
|
||||
meta.element.setAttribute('data-content-type', result.type);
|
||||
console.log(`✅ Content saved: ${result.id}`, contentValue);
|
||||
} else {
|
||||
// Non-enhanced site - create with backend ID generation
|
||||
const contentType = this.determineContentType(meta.element);
|
||||
const result = await this.apiClient.createContent(null, contentValue, contentType, meta.elementContext);
|
||||
|
||||
if (result) {
|
||||
// Store the backend-generated ID in the element
|
||||
meta.element.setAttribute('data-content-id', result.id);
|
||||
meta.element.setAttribute('data-content-type', result.type);
|
||||
console.log(`✅ Content created with backend ID: ${result.id}`, contentValue);
|
||||
} else {
|
||||
console.error('❌ Failed to save content to server');
|
||||
}
|
||||
console.error('❌ Failed to save content to server');
|
||||
}
|
||||
|
||||
// Close form
|
||||
|
||||
@@ -106,24 +106,14 @@ export class InsertrCore {
|
||||
getElementMetadata(element) {
|
||||
const existingId = element.getAttribute('data-content-id');
|
||||
|
||||
if (existingId) {
|
||||
// Enhanced site - use existing ID
|
||||
return {
|
||||
contentId: existingId,
|
||||
contentType: element.getAttribute('data-content-type') || this.detectContentType(element),
|
||||
element: element,
|
||||
hasExistingId: true
|
||||
};
|
||||
} else {
|
||||
// Non-enhanced site - prepare context for backend ID generation
|
||||
return {
|
||||
contentId: null, // Backend will generate
|
||||
contentType: this.detectContentType(element),
|
||||
element: element,
|
||||
elementContext: this.extractElementContext(element),
|
||||
hasExistingId: false
|
||||
};
|
||||
}
|
||||
// Always provide both existing ID (if any) and element context
|
||||
// Backend will use existing ID if provided, or generate new one from context
|
||||
return {
|
||||
contentId: existingId, // null if new content, existing ID if updating
|
||||
contentType: element.getAttribute('data-content-type') || this.detectContentType(element),
|
||||
element: element,
|
||||
elementContext: this.extractElementContext(element)
|
||||
};
|
||||
}
|
||||
|
||||
// Extract element context for backend ID generation
|
||||
|
||||
Reference in New Issue
Block a user