feat: implement unified editor with content persistence and server-side upsert
- Replace dual update systems with single markdown-first editor architecture - Add server-side upsert to eliminate 404 errors on PUT operations - Fix content persistence race condition between preview and save operations - Remove legacy updateElementContent system entirely - Add comprehensive authentication with JWT scaffolding and dev mode - Implement EditContext.updateOriginalContent() for proper baseline management - Enable markdown formatting in all text elements (h1-h6, p, div, etc) - Clean terminology: remove 'unified' references from codebase Technical changes: * core/editor.js: Remove legacy update system, unify content types as markdown * ui/Editor.js: Add updateOriginalContent() method to fix save persistence * ui/Previewer.js: Clean live preview system for all content types * api/handlers.go: Implement UpsertContent for idempotent PUT operations * auth/*: Complete authentication service with OAuth scaffolding * db/queries/content.sql: Add upsert query with ON CONFLICT handling * Schema: Remove type constraints, rely on server-side validation Result: Clean content editing with persistent saves, no 404 errors, markdown support in all text elements
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/insertr/insertr/internal/auth"
|
||||
"github.com/insertr/insertr/internal/db"
|
||||
"github.com/insertr/insertr/internal/db/postgresql"
|
||||
"github.com/insertr/insertr/internal/db/sqlite"
|
||||
@@ -18,13 +19,15 @@ import (
|
||||
|
||||
// ContentHandler handles all content-related HTTP requests
|
||||
type ContentHandler struct {
|
||||
database *db.Database
|
||||
database *db.Database
|
||||
authService *auth.AuthService
|
||||
}
|
||||
|
||||
// NewContentHandler creates a new content handler
|
||||
func NewContentHandler(database *db.Database) *ContentHandler {
|
||||
func NewContentHandler(database *db.Database, authService *auth.AuthService) *ContentHandler {
|
||||
return &ContentHandler{
|
||||
database: database,
|
||||
database: database,
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,14 +176,13 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
|
||||
siteID = "default" // final fallback
|
||||
}
|
||||
|
||||
// Extract user from request (for now, use X-User-ID header or fallback)
|
||||
userID := r.Header.Get("X-User-ID")
|
||||
if userID == "" && req.CreatedBy != "" {
|
||||
userID = req.CreatedBy
|
||||
}
|
||||
if userID == "" {
|
||||
userID = "anonymous"
|
||||
// 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
|
||||
|
||||
var content interface{}
|
||||
var err error
|
||||
@@ -219,7 +221,7 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(item)
|
||||
}
|
||||
|
||||
// UpdateContent handles PUT /api/content/{id}
|
||||
// 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"]
|
||||
@@ -236,29 +238,70 @@ func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract user from request
|
||||
userID := r.Header.Get("X-User-ID")
|
||||
if userID == "" && req.UpdatedBy != "" {
|
||||
userID = req.UpdatedBy
|
||||
// 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
|
||||
}
|
||||
if userID == "" {
|
||||
userID = "anonymous"
|
||||
userID := userInfo.ID
|
||||
|
||||
// Check if content exists for version history (non-blocking)
|
||||
var existingContent interface{}
|
||||
var contentExists bool
|
||||
|
||||
switch h.database.GetDBType() {
|
||||
case "sqlite3":
|
||||
existingContent, _ = h.database.GetSQLiteQueries().GetContent(context.Background(), sqlite.GetContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
})
|
||||
contentExists = existingContent != nil
|
||||
case "postgresql":
|
||||
existingContent, _ = h.database.GetPostgreSQLQueries().GetContent(context.Background(), postgresql.GetContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
})
|
||||
contentExists = existingContent != nil
|
||||
}
|
||||
|
||||
// Get current content for version history and type preservation
|
||||
var currentContent interface{}
|
||||
// Archive existing version before upsert (only if content already exists)
|
||||
if contentExists {
|
||||
if err := h.createContentVersion(existingContent); err != nil {
|
||||
// Log error but don't fail the request - version history is non-critical
|
||||
fmt.Printf("Warning: Failed to create content version: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine content type: use provided type, fallback to existing type, default to "text"
|
||||
contentType := req.Type
|
||||
if contentType == "" && contentExists {
|
||||
contentType = h.getContentType(existingContent)
|
||||
}
|
||||
if contentType == "" {
|
||||
contentType = "text" // default type for new content
|
||||
}
|
||||
|
||||
// Perform upsert operation
|
||||
var upsertedContent interface{}
|
||||
var err error
|
||||
|
||||
switch h.database.GetDBType() {
|
||||
case "sqlite3":
|
||||
currentContent, err = h.database.GetSQLiteQueries().GetContent(context.Background(), sqlite.GetContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
upsertedContent, err = h.database.GetSQLiteQueries().UpsertContent(context.Background(), sqlite.UpsertContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
Value: req.Value,
|
||||
Type: contentType,
|
||||
LastEditedBy: userID,
|
||||
})
|
||||
case "postgresql":
|
||||
currentContent, err = h.database.GetPostgreSQLQueries().GetContent(context.Background(), postgresql.GetContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
upsertedContent, err = h.database.GetPostgreSQLQueries().UpsertContent(context.Background(), postgresql.UpsertContentParams{
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
Value: req.Value,
|
||||
Type: contentType,
|
||||
LastEditedBy: userID,
|
||||
})
|
||||
default:
|
||||
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
|
||||
@@ -266,58 +309,11 @@ func (h *ContentHandler) UpdateContent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Content not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
|
||||
http.Error(w, fmt.Sprintf("Failed to upsert content: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Archive current version before updating
|
||||
err = h.createContentVersion(currentContent)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to create version: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine content type
|
||||
contentType := req.Type
|
||||
if contentType == "" {
|
||||
contentType = h.getContentType(currentContent) // preserve existing type if not specified
|
||||
}
|
||||
|
||||
// Update the content
|
||||
var updatedContent interface{}
|
||||
|
||||
switch h.database.GetDBType() {
|
||||
case "sqlite3":
|
||||
updatedContent, err = h.database.GetSQLiteQueries().UpdateContent(context.Background(), sqlite.UpdateContentParams{
|
||||
Value: req.Value,
|
||||
Type: contentType,
|
||||
LastEditedBy: userID,
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
})
|
||||
case "postgresql":
|
||||
updatedContent, err = h.database.GetPostgreSQLQueries().UpdateContent(context.Background(), postgresql.UpdateContentParams{
|
||||
Value: req.Value,
|
||||
Type: contentType,
|
||||
LastEditedBy: userID,
|
||||
ID: contentID,
|
||||
SiteID: siteID,
|
||||
})
|
||||
default:
|
||||
http.Error(w, "Unsupported database type", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to update content: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
item := h.convertToAPIContent(updatedContent)
|
||||
item := h.convertToAPIContent(upsertedContent)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(item)
|
||||
@@ -459,14 +455,13 @@ func (h *ContentHandler) RollbackContent(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract user from request
|
||||
userID := r.Header.Get("X-User-ID")
|
||||
if userID == "" && req.RolledBackBy != "" {
|
||||
userID = req.RolledBackBy
|
||||
}
|
||||
if userID == "" {
|
||||
userID = "anonymous"
|
||||
// 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
|
||||
|
||||
// Archive current version before rollback
|
||||
var currentContent interface{}
|
||||
|
||||
265
internal/auth/auth.go
Normal file
265
internal/auth/auth.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// UserInfo represents authenticated user information
|
||||
type UserInfo struct {
|
||||
ID string `json:"sub"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Provider string `json:"iss,omitempty"`
|
||||
}
|
||||
|
||||
// AuthConfig holds authentication configuration
|
||||
type AuthConfig struct {
|
||||
DevMode bool
|
||||
JWTSecret string
|
||||
OAuthConfigs map[string]OAuthConfig
|
||||
}
|
||||
|
||||
// OAuthConfig holds OAuth provider configuration
|
||||
type OAuthConfig struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
RedirectURL string
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
// AuthService handles authentication operations
|
||||
type AuthService struct {
|
||||
config *AuthConfig
|
||||
}
|
||||
|
||||
// NewAuthService creates a new authentication service
|
||||
func NewAuthService(config *AuthConfig) *AuthService {
|
||||
return &AuthService{config: config}
|
||||
}
|
||||
|
||||
// ExtractUserFromRequest extracts user information from HTTP request
|
||||
func (a *AuthService) ExtractUserFromRequest(r *http.Request) (*UserInfo, error) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return &UserInfo{ID: "anonymous"}, nil
|
||||
}
|
||||
|
||||
// Parse Bearer token
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return nil, fmt.Errorf("invalid authorization header format")
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// Handle mock tokens in development mode
|
||||
if a.config.DevMode && strings.HasPrefix(token, "mock-") {
|
||||
return a.parseMockToken(token), nil
|
||||
}
|
||||
|
||||
// Parse real JWT token
|
||||
return a.parseJWT(token)
|
||||
}
|
||||
|
||||
// parseMockToken handles mock development tokens
|
||||
func (a *AuthService) parseMockToken(token string) *UserInfo {
|
||||
// Mock token format: mock-{user}-{timestamp}-{random}
|
||||
parts := strings.Split(token, "-")
|
||||
if len(parts) >= 2 {
|
||||
return &UserInfo{
|
||||
ID: parts[1], // user part
|
||||
Email: fmt.Sprintf("%s@localhost", parts[1]),
|
||||
Name: strings.Title(parts[1]),
|
||||
Provider: "insertr-dev",
|
||||
}
|
||||
}
|
||||
|
||||
return &UserInfo{ID: "anonymous"}
|
||||
}
|
||||
|
||||
// parseJWT parses and validates a real JWT token
|
||||
func (a *AuthService) parseJWT(tokenString string) (*UserInfo, error) {
|
||||
// Parse the token
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
// Validate signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(a.config.JWTSecret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JWT: %w", err)
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
userInfo := &UserInfo{}
|
||||
|
||||
// Extract standard JWT claims
|
||||
if sub, ok := claims["sub"].(string); ok {
|
||||
userInfo.ID = sub
|
||||
}
|
||||
if email, ok := claims["email"].(string); ok {
|
||||
userInfo.Email = email
|
||||
}
|
||||
if name, ok := claims["name"].(string); ok {
|
||||
userInfo.Name = name
|
||||
}
|
||||
if iss, ok := claims["iss"].(string); ok {
|
||||
userInfo.Provider = iss
|
||||
}
|
||||
|
||||
// Fallback to alternative claim names
|
||||
if userInfo.ID == "" {
|
||||
if userID, ok := claims["user_id"].(string); ok {
|
||||
userInfo.ID = userID
|
||||
}
|
||||
}
|
||||
|
||||
// Default to anonymous if no user ID found
|
||||
if userInfo.ID == "" {
|
||||
userInfo.ID = "anonymous"
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid JWT claims")
|
||||
}
|
||||
|
||||
// CreateMockJWT creates a mock JWT token for development/testing
|
||||
func (a *AuthService) CreateMockJWT(userID, email, name string) (string, error) {
|
||||
if !a.config.DevMode {
|
||||
return "", fmt.Errorf("mock JWT creation only allowed in development mode")
|
||||
}
|
||||
|
||||
// Create the claims
|
||||
claims := jwt.MapClaims{
|
||||
"sub": userID,
|
||||
"email": email,
|
||||
"name": name,
|
||||
"iss": "insertr-dev",
|
||||
"iat": time.Now().Unix(),
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||
}
|
||||
|
||||
// Create token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
// Sign and get the complete encoded token as a string
|
||||
tokenString, err := token.SignedString([]byte(a.config.JWTSecret))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign JWT: %w", err)
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT token without extracting user info
|
||||
func (a *AuthService) ValidateToken(tokenString string) error {
|
||||
_, err := a.parseJWT(tokenString)
|
||||
return err
|
||||
}
|
||||
|
||||
// RefreshToken creates a new token with extended expiration
|
||||
func (a *AuthService) RefreshToken(tokenString string) (string, error) {
|
||||
userInfo, err := a.parseJWT(tokenString)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot refresh invalid token: %w", err)
|
||||
}
|
||||
|
||||
// Create new token with same user info but extended expiration
|
||||
claims := jwt.MapClaims{
|
||||
"sub": userInfo.ID,
|
||||
"email": userInfo.Email,
|
||||
"name": userInfo.Name,
|
||||
"iss": userInfo.Provider,
|
||||
"iat": time.Now().Unix(),
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(a.config.JWTSecret))
|
||||
}
|
||||
|
||||
// IsAuthenticated checks if the request has valid authentication
|
||||
func (a *AuthService) IsAuthenticated(r *http.Request) bool {
|
||||
userInfo, err := a.ExtractUserFromRequest(r)
|
||||
return err == nil && userInfo.ID != "anonymous"
|
||||
}
|
||||
|
||||
// RequireAuth middleware that requires authentication
|
||||
func (a *AuthService) RequireAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userInfo, err := a.ExtractUserFromRequest(r)
|
||||
if err != nil || userInfo.ID == "anonymous" {
|
||||
http.Error(w, "Authentication required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Add user info to request context for use by handlers
|
||||
ctx := r.Context()
|
||||
ctx = ContextWithUser(ctx, userInfo)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// OAuth flow handlers (to be implemented when adding real OAuth)
|
||||
|
||||
// HandleOAuthLogin initiates OAuth flow
|
||||
func (a *AuthService) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) {
|
||||
provider := r.URL.Query().Get("provider")
|
||||
if provider == "" {
|
||||
provider = "google"
|
||||
}
|
||||
|
||||
// TODO: Implement OAuth initiation
|
||||
// For now, return mock success in dev mode
|
||||
if a.config.DevMode {
|
||||
response := map[string]interface{}{
|
||||
"message": "OAuth login not yet implemented",
|
||||
"redirect_url": "/auth/callback?code=mock_code&state=mock_state",
|
||||
"dev_mode": true,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "OAuth not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// HandleOAuthCallback handles OAuth provider callback
|
||||
func (a *AuthService) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.URL.Query().Get("code")
|
||||
_ = r.URL.Query().Get("state") // TODO: validate state parameter
|
||||
|
||||
// TODO: Implement OAuth token exchange
|
||||
// For now, return mock token in dev mode
|
||||
if a.config.DevMode && code != "" {
|
||||
mockToken, err := a.CreateMockJWT("dev-user", "dev@localhost", "Development User")
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create mock token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"token": mockToken,
|
||||
"dev_mode": true,
|
||||
"message": "Mock OAuth callback successful",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "OAuth callback not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
29
internal/auth/context.go
Normal file
29
internal/auth/context.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package auth
|
||||
|
||||
import "context"
|
||||
|
||||
// Context keys for user information
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
userInfoContextKey contextKey = "user_info"
|
||||
)
|
||||
|
||||
// ContextWithUser adds user information to request context
|
||||
func ContextWithUser(ctx context.Context, user *UserInfo) context.Context {
|
||||
return context.WithValue(ctx, userInfoContextKey, user)
|
||||
}
|
||||
|
||||
// UserFromContext extracts user information from request context
|
||||
func UserFromContext(ctx context.Context) (*UserInfo, bool) {
|
||||
user, ok := ctx.Value(userInfoContextKey).(*UserInfo)
|
||||
return user, ok
|
||||
}
|
||||
|
||||
// UserIDFromContext extracts user ID from request context
|
||||
func UserIDFromContext(ctx context.Context) string {
|
||||
if user, ok := UserFromContext(ctx); ok && user != nil {
|
||||
return user.ID
|
||||
}
|
||||
return "anonymous"
|
||||
}
|
||||
@@ -212,3 +212,42 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const upsertContent = `-- name: UpsertContent :one
|
||||
INSERT INTO content (id, site_id, value, type, last_edited_by)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT(id, site_id) DO UPDATE SET
|
||||
value = EXCLUDED.value,
|
||||
type = EXCLUDED.type,
|
||||
last_edited_by = EXCLUDED.last_edited_by
|
||||
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by
|
||||
`
|
||||
|
||||
type UpsertContentParams struct {
|
||||
ID string `json:"id"`
|
||||
SiteID string `json:"site_id"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
LastEditedBy string `json:"last_edited_by"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) {
|
||||
row := q.db.QueryRowContext(ctx, upsertContent,
|
||||
arg.ID,
|
||||
arg.SiteID,
|
||||
arg.Value,
|
||||
arg.Type,
|
||||
arg.LastEditedBy,
|
||||
)
|
||||
var i Content
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SiteID,
|
||||
&i.Value,
|
||||
&i.Type,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LastEditedBy,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type Querier interface {
|
||||
InitializeSchema(ctx context.Context) error
|
||||
InitializeVersionsTable(ctx context.Context) error
|
||||
UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error)
|
||||
UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error)
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
|
||||
@@ -212,3 +212,42 @@ func (q *Queries) UpdateContent(ctx context.Context, arg UpdateContentParams) (C
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const upsertContent = `-- name: UpsertContent :one
|
||||
INSERT INTO content (id, site_id, value, type, last_edited_by)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
ON CONFLICT(id, site_id) DO UPDATE SET
|
||||
value = EXCLUDED.value,
|
||||
type = EXCLUDED.type,
|
||||
last_edited_by = EXCLUDED.last_edited_by
|
||||
RETURNING id, site_id, value, type, created_at, updated_at, last_edited_by
|
||||
`
|
||||
|
||||
type UpsertContentParams struct {
|
||||
ID string `json:"id"`
|
||||
SiteID string `json:"site_id"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
LastEditedBy string `json:"last_edited_by"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error) {
|
||||
row := q.db.QueryRowContext(ctx, upsertContent,
|
||||
arg.ID,
|
||||
arg.SiteID,
|
||||
arg.Value,
|
||||
arg.Type,
|
||||
arg.LastEditedBy,
|
||||
)
|
||||
var i Content
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SiteID,
|
||||
&i.Value,
|
||||
&i.Type,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LastEditedBy,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ type Querier interface {
|
||||
InitializeSchema(ctx context.Context) error
|
||||
InitializeVersionsTable(ctx context.Context) error
|
||||
UpdateContent(ctx context.Context, arg UpdateContentParams) (Content, error)
|
||||
UpsertContent(ctx context.Context, arg UpsertContentParams) (Content, error)
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
|
||||
Reference in New Issue
Block a user