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:
2025-09-11 16:36:42 +02:00
parent 2ce37874ff
commit 3db1340cce
8 changed files with 65 additions and 177 deletions

View File

@@ -130,7 +130,7 @@ func runServe(cmd *cobra.Command, args []string) {
contentRouter := apiRouter.PathPrefix("/content").Subrouter() contentRouter := apiRouter.PathPrefix("/content").Subrouter()
contentRouter.HandleFunc("/bulk", contentHandler.GetBulkContent).Methods("GET") contentRouter.HandleFunc("/bulk", contentHandler.GetBulkContent).Methods("GET")
contentRouter.HandleFunc("/{id}", contentHandler.GetContent).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.GetAllContent).Methods("GET")
contentRouter.HandleFunc("", contentHandler.CreateContent).Methods("POST") contentRouter.HandleFunc("", contentHandler.CreateContent).Methods("POST")

View File

@@ -9,7 +9,7 @@
<!-- Navigation --> <!-- Navigation -->
<nav class="navbar"> <nav class="navbar">
<div class="container"> <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"> <ul class="nav-links">
<li><a href="index.html">Home</a></li> <li><a href="index.html">Home</a></li>
<li><a href="about.html">About</a></li> <li><a href="about.html">About</a></li>
@@ -22,15 +22,15 @@
<!-- Hero Section --> <!-- Hero Section -->
<section class="hero"> <section class="hero">
<div class="container"> <div class="container">
<h1 class="insertr" >About Acme Consulting</h1> <h1 class="insertr" data-content-id="hero-title-c70343" data-content-type="text">About Acme Consulting</h1>
<p class="lead insertr" >We&#39;re a team of experienced consultants dedicated to helping small businesses thrive in today&#39;s competitive marketplace.</p> <p class="lead insertr" data-content-id="hero-lead-673026" data-content-type="markdown">We&#39;re a team of experienced consultants dedicated to helping small businesses thrive in today&#39;s competitive marketplace.</p>
</div> </div>
</section> </section>
<!-- Story Section --> <!-- Story Section -->
<section class="services"> <section class="services">
<div class="container"> <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"> <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> <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 --> <!-- Team Section -->
<section class="cta"> <section class="cta">
<div class="container"> <div class="container">
<h2 class="insertr" >Our Team</h2> <h2 class="insertr" data-content-id="subtitle-ba6444" data-content-type="text">Our Team</h2>
<p class="insertr" >We&#39;re a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p> <p class="insertr" data-content-id="text-8b3502" data-content-type="markdown">We&#39;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="services-grid" style="margin-top: 3rem;">
<div class="service-card"> <div class="service-card">
@@ -76,19 +76,19 @@
<!-- Values Section --> <!-- Values Section -->
<section class="testimonial"> <section class="testimonial">
<div class="container"> <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 style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; text-align: left;">
<div> <div>
<h3 class="insertr" >Client-First</h3> <h3 class="insertr" data-content-id="testimonial-heading-f7eefa" data-content-type="text">Client-First</h3>
<p class="insertr" >Every recommendation we make is designed with your specific business context and goals in mind.</p> <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>
<div> <div>
<h3 class="insertr" >Practical Solutions</h3> <h3 class="insertr" data-content-id="testimonial-heading-fd4293" data-content-type="text">Practical Solutions</h3>
<p class="insertr" >We believe in strategies that you can actually implement with your current resources and capabilities.</p> <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>
<div> <div>
<h3 class="insertr" >Long-term Partnership</h3> <h3 class="insertr" data-content-id="testimonial-heading-957cc8" data-content-type="text">Long-term Partnership</h3>
<p class="insertr" >We&#39;re not just consultants; we&#39;re partners in your business success for the long haul.</p> <p class="insertr" data-content-id="testimonial-text-45dae2" data-content-type="markdown">We&#39;re not just consultants; we&#39;re partners in your business success for the long haul.</p>
</div> </div>
</div> </div>
</div> </div>
@@ -97,7 +97,7 @@
<!-- Test Section for Insertr Features --> <!-- Test Section for Insertr Features -->
<section class="testimonial"> <section class="testimonial">
<div class="container"> <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) --> <!-- Test 1: .insertr container expansion (should make each p individually editable) -->
<div style="margin-bottom: 2rem;"> <div style="margin-bottom: 2rem;">
@@ -124,8 +124,8 @@
<!-- Footer --> <!-- Footer -->
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<p class="insertr" >© 2024 Acme Consulting Services. All rights reserved.</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" >📧 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-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> </div>
</footer> </footer>

View File

@@ -9,7 +9,7 @@
<!-- Navigation --> <!-- Navigation -->
<nav class="navbar"> <nav class="navbar">
<div class="container"> <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"> <ul class="nav-links">
<li><a href="index.html">Home</a></li> <li><a href="index.html">Home</a></li>
<li><a href="about.html">About</a></li> <li><a href="about.html">About</a></li>
@@ -22,30 +22,30 @@
<!-- Hero Section --> <!-- Hero Section -->
<section class="hero"> <section class="hero">
<div class="container"> <div class="container">
<h1 class="insertr" >!Transform Your Business with Expert Consulting!</h1> <h1 class="insertr" data-content-id="hero-title-a1de7b" data-content-type="text">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> <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" >Get Started Today?</a> <a href="contact.html" class="btn-primary insertr" data-content-id="hero-link-20f13f" data-content-type="link">Get Started Today</a>
</div> </div>
</section> </section>
<!-- Services Section --> <!-- Services Section -->
<section class="services"> <section class="services">
<div class="container"> <div class="container">
<h2 class="insertr" >Our Services</h2> <h2 class="insertr" data-content-id="services-subtitle-7a7725" data-content-type="text">Our Offers</h2>
<p class="section-subtitle insertr" >Comprehensive solutions tailored to your business needs</p> <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="services-grid">
<div class="service-card"> <div class="service-card">
<h3 class="insertr" >Strategic Planning</h3> <h3 class="insertr" data-content-id="services-heading-6dd5f2" data-content-type="text">Strategic Planning</h3>
<p class="insertr" >Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.</p> <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>
<div class="service-card"> <div class="service-card">
<h3 class="insertr" >Operations Optimization</h3> <h3 class="insertr" data-content-id="services-heading-fdebeb" data-content-type="text">Operations Optimization</h3>
<p class="insertr" >Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.</p> <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>
<div class="service-card"> <div class="service-card">
<h3 class="insertr" >Digital Transformation</h3> <h3 class="insertr" data-content-id="services-heading-0d6ef9" data-content-type="text">Digital Transformation</h3>
<p class="insertr" >Modernize your technology stack and digital presence to compete effectively in today&#39;s marketplace.</p> <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&#39;s marketplace.</p>
</div> </div>
</div> </div>
</div> </div>
@@ -55,8 +55,8 @@
<section class="testimonial"> <section class="testimonial">
<div class="container"> <div class="container">
<blockquote> <blockquote>
<p class="insertr" >&#34;Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations.&#34;</p> <p class="insertr" data-content-id="testimonial-text-69de1a" data-content-type="markdown">&#34;Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations.&#34;</p>
<cite class="insertr" >Sarah Johnson, CEO of TechStart Inc.</cite> <cite class="insertr" data-content-id="testimonial-content-dfd023" data-content-type="text">Sarah Johnson, CEO of TechStart Inc.</cite>
</blockquote> </blockquote>
</div> </div>
</section> </section>
@@ -64,17 +64,17 @@
<!-- Call to Action --> <!-- Call to Action -->
<section class="cta"> <section class="cta">
<div class="container"> <div class="container">
<h2 class="insertr" >Ready to Transform Your Business?</h2> <h2 class="insertr" data-content-id="subtitle-d0ebd3" data-content-type="text">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> <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" >Schedule Consultation</a> <a href="contact.html" class="btn-primary insertr" data-content-id="link-d11ae9" data-content-type="link">Schedule Consultation</a>
</div> </div>
</section> </section>
<!-- Footer --> <!-- Footer -->
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<p class="insertr" >© 2024 Acme Consulting Services. All rights reserved.</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" >📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <a href="#" class="insertr-gate">Editor</a></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> </div>
</footer> </footer>

View File

@@ -252,68 +252,6 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
} }
userID := userInfo.ID 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) // Check if content exists for version history (non-blocking)
var existingContent interface{} var existingContent interface{}
var contentExists bool var contentExists bool
@@ -350,13 +288,12 @@ func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
contentType = "text" // default type for new content contentType = "text" // default type for new content
} }
// Perform upsert operation var content interface{}
var upsertedContent interface{}
var err error var err error
switch h.database.GetDBType() { switch h.database.GetDBType() {
case "sqlite3": case "sqlite3":
upsertedContent, err = h.database.GetSQLiteQueries().UpsertContent(context.Background(), sqlite.UpsertContentParams{ content, err = h.database.GetSQLiteQueries().UpsertContent(context.Background(), sqlite.UpsertContentParams{
ID: contentID, ID: contentID,
SiteID: siteID, SiteID: siteID,
Value: req.Value, Value: req.Value,
@@ -364,7 +301,7 @@ func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
LastEditedBy: userID, LastEditedBy: userID,
}) })
case "postgresql": case "postgresql":
upsertedContent, err = h.database.GetPostgreSQLQueries().UpsertContent(context.Background(), postgresql.UpsertContentParams{ content, err = h.database.GetPostgreSQLQueries().UpsertContent(context.Background(), postgresql.UpsertContentParams{
ID: contentID, ID: contentID,
SiteID: siteID, SiteID: siteID,
Value: req.Value, Value: req.Value,
@@ -381,7 +318,7 @@ func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
return return
} }
item := h.convertToAPIContent(upsertedContent) item := h.convertToAPIContent(content)
// Trigger file enhancement if site is registered for auto-enhancement // Trigger file enhancement if site is registered for auto-enhancement
if h.siteManager != nil && h.siteManager.IsAutoEnhanceEnabled(siteID) { 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.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(item) json.NewEncoder(w).Encode(item)
} }

View File

@@ -50,12 +50,6 @@ type CreateContentRequest struct {
CreatedBy string `json:"created_by,omitempty"` 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 { 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"`

View File

@@ -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) { async createContent(contentId, content, type, elementContext = null) {
try { try {
const payload = { const payload = {

View File

@@ -102,27 +102,22 @@ export class InsertrEditor {
contentValue = formData.text || formData; contentValue = formData.text || formData;
} }
if (meta.hasExistingId) { // Universal upsert - works for both new and existing content
// Enhanced site - update existing content const contentType = this.determineContentType(meta.element);
const updateSuccess = await this.apiClient.updateContent(meta.contentId, contentValue); const result = await this.apiClient.createContent(
if (!updateSuccess) { meta.contentId, // Use existing ID if available, null if new
console.error('❌ Failed to update content:', meta.contentId); contentValue,
} else { contentType,
console.log(`✅ Content updated:`, meta.contentId, contentValue); 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 { } else {
// Non-enhanced site - create with backend ID generation console.error('❌ Failed to save content to server');
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');
}
} }
// Close form // Close form

View File

@@ -106,24 +106,14 @@ export class InsertrCore {
getElementMetadata(element) { getElementMetadata(element) {
const existingId = element.getAttribute('data-content-id'); const existingId = element.getAttribute('data-content-id');
if (existingId) { // Always provide both existing ID (if any) and element context
// Enhanced site - use existing ID // Backend will use existing ID if provided, or generate new one from context
return { return {
contentId: existingId, contentId: existingId, // null if new content, existing ID if updating
contentType: element.getAttribute('data-content-type') || this.detectContentType(element), contentType: element.getAttribute('data-content-type') || this.detectContentType(element),
element: element, element: element,
hasExistingId: true elementContext: this.extractElementContext(element)
}; };
} 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
};
}
} }
// Extract element context for backend ID generation // Extract element context for backend ID generation