mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-01-10 05:04:25 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62a8e56f1b | ||
|
|
d8df1bdac2 | ||
|
|
c0c669bd3d |
@@ -12,6 +12,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/common"
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
|
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
|
||||||
@@ -123,27 +125,6 @@ func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFun
|
|||||||
ComplexAPI: complexAPI,
|
ComplexAPI: complexAPI,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute BeforeQueryList hook
|
|
||||||
if err := h.hooks.Execute(BeforeQueryList, hookCtx); err != nil {
|
|
||||||
logger.Error("BeforeQueryList hook failed: %v", err)
|
|
||||||
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if hook aborted the operation
|
|
||||||
if hookCtx.Abort {
|
|
||||||
if hookCtx.AbortCode == 0 {
|
|
||||||
hookCtx.AbortCode = http.StatusBadRequest
|
|
||||||
}
|
|
||||||
sendError(w, hookCtx.AbortCode, "operation_aborted", hookCtx.AbortMessage, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use potentially modified SQL query and variables from hooks
|
|
||||||
sqlquery = hookCtx.SQLQuery
|
|
||||||
variables = hookCtx.Variables
|
|
||||||
// complexAPI = hookCtx.ComplexAPI
|
|
||||||
|
|
||||||
// Extract input variables from SQL query (placeholders like [variable])
|
// Extract input variables from SQL query (placeholders like [variable])
|
||||||
sqlquery = h.extractInputVariables(sqlquery, &inputvars)
|
sqlquery = h.extractInputVariables(sqlquery, &inputvars)
|
||||||
|
|
||||||
@@ -203,6 +184,27 @@ func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFun
|
|||||||
|
|
||||||
// Execute query within transaction
|
// Execute query within transaction
|
||||||
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
|
// Set transaction in hook context for hooks to use
|
||||||
|
hookCtx.Tx = tx
|
||||||
|
|
||||||
|
// Execute BeforeQueryList hook (inside transaction)
|
||||||
|
if err := h.hooks.Execute(BeforeQueryList, hookCtx); err != nil {
|
||||||
|
logger.Error("BeforeQueryList hook failed: %v", err)
|
||||||
|
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if hook aborted the operation
|
||||||
|
if hookCtx.Abort {
|
||||||
|
if hookCtx.AbortCode == 0 {
|
||||||
|
hookCtx.AbortCode = http.StatusBadRequest
|
||||||
|
}
|
||||||
|
sendError(w, hookCtx.AbortCode, "operation_aborted", hookCtx.AbortMessage, nil)
|
||||||
|
return fmt.Errorf("operation aborted: %s", hookCtx.AbortMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use potentially modified SQL query from hook
|
||||||
|
sqlquery = hookCtx.SQLQuery
|
||||||
sqlqueryCnt := sqlquery
|
sqlqueryCnt := sqlquery
|
||||||
|
|
||||||
// Parse sorting and pagination parameters
|
// Parse sorting and pagination parameters
|
||||||
@@ -286,6 +288,21 @@ func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFun
|
|||||||
}
|
}
|
||||||
total = hookCtx.Total
|
total = hookCtx.Total
|
||||||
|
|
||||||
|
// Execute AfterQueryList hook (inside transaction)
|
||||||
|
hookCtx.Result = dbobjlist
|
||||||
|
hookCtx.Total = total
|
||||||
|
hookCtx.Error = nil
|
||||||
|
if err := h.hooks.Execute(AfterQueryList, hookCtx); err != nil {
|
||||||
|
logger.Error("AfterQueryList hook failed: %v", err)
|
||||||
|
sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Use potentially modified result from hook
|
||||||
|
if modifiedResult, ok := hookCtx.Result.([]map[string]interface{}); ok {
|
||||||
|
dbobjlist = modifiedResult
|
||||||
|
}
|
||||||
|
total = hookCtx.Total
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -294,21 +311,6 @@ func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFun
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute AfterQueryList hook
|
|
||||||
hookCtx.Result = dbobjlist
|
|
||||||
hookCtx.Total = total
|
|
||||||
hookCtx.Error = err
|
|
||||||
if err := h.hooks.Execute(AfterQueryList, hookCtx); err != nil {
|
|
||||||
logger.Error("AfterQueryList hook failed: %v", err)
|
|
||||||
sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Use potentially modified result from hook
|
|
||||||
if modifiedResult, ok := hookCtx.Result.([]map[string]interface{}); ok {
|
|
||||||
dbobjlist = modifiedResult
|
|
||||||
}
|
|
||||||
total = hookCtx.Total
|
|
||||||
|
|
||||||
// Set response headers
|
// Set response headers
|
||||||
respOffset := 0
|
respOffset := 0
|
||||||
if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
|
if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
|
||||||
@@ -459,26 +461,6 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
|
|||||||
ComplexAPI: complexAPI,
|
ComplexAPI: complexAPI,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute BeforeQuery hook
|
|
||||||
if err := h.hooks.Execute(BeforeQuery, hookCtx); err != nil {
|
|
||||||
logger.Error("BeforeQuery hook failed: %v", err)
|
|
||||||
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if hook aborted the operation
|
|
||||||
if hookCtx.Abort {
|
|
||||||
if hookCtx.AbortCode == 0 {
|
|
||||||
hookCtx.AbortCode = http.StatusBadRequest
|
|
||||||
}
|
|
||||||
sendError(w, hookCtx.AbortCode, "operation_aborted", hookCtx.AbortMessage, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use potentially modified SQL query and variables from hooks
|
|
||||||
sqlquery = hookCtx.SQLQuery
|
|
||||||
variables = hookCtx.Variables
|
|
||||||
|
|
||||||
// Extract input variables from SQL query
|
// Extract input variables from SQL query
|
||||||
sqlquery = h.extractInputVariables(sqlquery, &inputvars)
|
sqlquery = h.extractInputVariables(sqlquery, &inputvars)
|
||||||
|
|
||||||
@@ -554,6 +536,28 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
|
|||||||
|
|
||||||
// Execute query within transaction
|
// Execute query within transaction
|
||||||
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
|
// Set transaction in hook context for hooks to use
|
||||||
|
hookCtx.Tx = tx
|
||||||
|
|
||||||
|
// Execute BeforeQuery hook (inside transaction)
|
||||||
|
if err := h.hooks.Execute(BeforeQuery, hookCtx); err != nil {
|
||||||
|
logger.Error("BeforeQuery hook failed: %v", err)
|
||||||
|
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if hook aborted the operation
|
||||||
|
if hookCtx.Abort {
|
||||||
|
if hookCtx.AbortCode == 0 {
|
||||||
|
hookCtx.AbortCode = http.StatusBadRequest
|
||||||
|
}
|
||||||
|
sendError(w, hookCtx.AbortCode, "operation_aborted", hookCtx.AbortMessage, nil)
|
||||||
|
return fmt.Errorf("operation aborted: %s", hookCtx.AbortMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use potentially modified SQL query from hook
|
||||||
|
sqlquery = hookCtx.SQLQuery
|
||||||
|
|
||||||
// Execute BeforeSQLExec hook
|
// Execute BeforeSQLExec hook
|
||||||
if err := h.hooks.Execute(BeforeSQLExec, hookCtx); err != nil {
|
if err := h.hooks.Execute(BeforeSQLExec, hookCtx); err != nil {
|
||||||
logger.Error("BeforeSQLExec hook failed: %v", err)
|
logger.Error("BeforeSQLExec hook failed: %v", err)
|
||||||
@@ -586,6 +590,19 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
|
|||||||
dbobj = modifiedResult
|
dbobj = modifiedResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute AfterQuery hook (inside transaction)
|
||||||
|
hookCtx.Result = dbobj
|
||||||
|
hookCtx.Error = nil
|
||||||
|
if err := h.hooks.Execute(AfterQuery, hookCtx); err != nil {
|
||||||
|
logger.Error("AfterQuery hook failed: %v", err)
|
||||||
|
sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Use potentially modified result from hook
|
||||||
|
if modifiedResult, ok := hookCtx.Result.(map[string]interface{}); ok {
|
||||||
|
dbobj = modifiedResult
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -594,19 +611,6 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute AfterQuery hook
|
|
||||||
hookCtx.Result = dbobj
|
|
||||||
hookCtx.Error = err
|
|
||||||
if err := h.hooks.Execute(AfterQuery, hookCtx); err != nil {
|
|
||||||
logger.Error("AfterQuery hook failed: %v", err)
|
|
||||||
sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Use potentially modified result from hook
|
|
||||||
if modifiedResult, ok := hookCtx.Result.(map[string]interface{}); ok {
|
|
||||||
dbobj = modifiedResult
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute BeforeResponse hook
|
// Execute BeforeResponse hook
|
||||||
hookCtx.Result = dbobj
|
hookCtx.Result = dbobj
|
||||||
if err := h.hooks.Execute(BeforeResponse, hookCtx); err != nil {
|
if err := h.hooks.Execute(BeforeResponse, hookCtx); err != nil {
|
||||||
@@ -1097,9 +1101,25 @@ func normalizePostgresValue(value interface{}) interface{} {
|
|||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
// Recursively normalize nested maps
|
// Recursively normalize nested maps
|
||||||
return normalizePostgresTypes(v)
|
return normalizePostgresTypes(v)
|
||||||
|
case string:
|
||||||
|
var jsonObj interface{}
|
||||||
|
if err := json.Unmarshal([]byte(v), &jsonObj); err == nil {
|
||||||
|
// It's valid JSON, return as json.RawMessage so it's not double-encoded
|
||||||
|
return json.RawMessage(v)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
case uuid.UUID:
|
||||||
|
return v.String()
|
||||||
|
case time.Time:
|
||||||
|
return v.Format(time.RFC3339)
|
||||||
|
case bool, int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64:
|
||||||
|
return v
|
||||||
default:
|
default:
|
||||||
// For other types (int, float, string, bool, etc.), return as-is
|
// For other types (int, float, bool, etc.), return as-is
|
||||||
|
// Check stringers
|
||||||
|
if str, ok := v.(fmt.Stringer); ok {
|
||||||
|
return str.String()
|
||||||
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/security"
|
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||||
)
|
)
|
||||||
@@ -46,6 +47,10 @@ type HookContext struct {
|
|||||||
// User context
|
// User context
|
||||||
UserContext *security.UserContext
|
UserContext *security.UserContext
|
||||||
|
|
||||||
|
// Tx provides access to the database/transaction for executing additional SQL
|
||||||
|
// This allows hooks to run custom queries in addition to the main Query chain
|
||||||
|
Tx common.Database
|
||||||
|
|
||||||
// Pagination and filtering (for list queries)
|
// Pagination and filtering (for list queries)
|
||||||
SortColumns string
|
SortColumns string
|
||||||
Limit int
|
Limit int
|
||||||
|
|||||||
@@ -47,3 +47,20 @@ func ExtractTableNameOnly(fullName string) string {
|
|||||||
|
|
||||||
return fullName[startIndex:]
|
return fullName[startIndex:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPointerElement returns the element type if the provided reflect.Type is a pointer.
|
||||||
|
// If the type is a slice of pointers, it returns the element type of the pointer within the slice.
|
||||||
|
// If neither condition is met, it returns the original type.
|
||||||
|
func GetPointerElement(v reflect.Type) reflect.Type {
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
return v.Elem()
|
||||||
|
}
|
||||||
|
if v.Kind() == reflect.Slice && v.Elem().Kind() == reflect.Ptr {
|
||||||
|
subElem := v.Elem()
|
||||||
|
if subElem.Elem().Kind() == reflect.Ptr {
|
||||||
|
return subElem.Elem().Elem()
|
||||||
|
}
|
||||||
|
return v.Elem()
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|||||||
@@ -698,20 +698,83 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Standard processing without nested relations
|
// Standard processing without nested relations
|
||||||
query := h.db.NewUpdate().Table(tableName).SetMap(updates)
|
// Get the primary key name
|
||||||
|
pkName := reflection.GetPrimaryKeyName(model)
|
||||||
|
|
||||||
// Apply conditions
|
// First, read the existing record from the database
|
||||||
|
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
||||||
|
selectQuery := h.db.NewSelect().Model(existingRecord)
|
||||||
|
|
||||||
|
// Apply conditions to select
|
||||||
if urlID != "" {
|
if urlID != "" {
|
||||||
logger.Debug("Updating by URL ID: %s", urlID)
|
logger.Debug("Updating by URL ID: %s", urlID)
|
||||||
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), urlID)
|
selectQuery = selectQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), urlID)
|
||||||
} else if reqID != nil {
|
} else if reqID != nil {
|
||||||
switch id := reqID.(type) {
|
switch id := reqID.(type) {
|
||||||
case string:
|
case string:
|
||||||
logger.Debug("Updating by request ID: %s", id)
|
logger.Debug("Updating by request ID: %s", id)
|
||||||
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), id)
|
selectQuery = selectQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||||
case []string:
|
case []string:
|
||||||
|
if len(id) > 0 {
|
||||||
logger.Debug("Updating by multiple IDs: %v", id)
|
logger.Debug("Updating by multiple IDs: %v", id)
|
||||||
query = query.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), id)
|
selectQuery = selectQuery.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(pkName)), id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := selectQuery.ScanModel(ctx); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
logger.Warn("No records found to update")
|
||||||
|
h.sendError(w, http.StatusNotFound, "not_found", "No records found to update", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Error("Error fetching existing record: %v", err)
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "update_error", "Error fetching existing record", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert existing record to map
|
||||||
|
existingMap := make(map[string]interface{})
|
||||||
|
jsonData, err := json.Marshal(existingRecord)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Error marshaling existing record: %v", err)
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "update_error", "Error processing existing record", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(jsonData, &existingMap); err != nil {
|
||||||
|
logger.Error("Error unmarshaling existing record: %v", err)
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "update_error", "Error processing existing record", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge only non-null and non-empty values from the incoming request into the existing record
|
||||||
|
for key, newValue := range updates {
|
||||||
|
// Skip if the value is nil
|
||||||
|
if newValue == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if the value is an empty string
|
||||||
|
if strVal, ok := newValue.(string); ok && strVal == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the existing map with the new value
|
||||||
|
existingMap[key] = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build update query with merged data
|
||||||
|
query := h.db.NewUpdate().Table(tableName).SetMap(existingMap)
|
||||||
|
|
||||||
|
// Apply conditions
|
||||||
|
if urlID != "" {
|
||||||
|
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), urlID)
|
||||||
|
} else if reqID != nil {
|
||||||
|
switch id := reqID.(type) {
|
||||||
|
case string:
|
||||||
|
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||||
|
case []string:
|
||||||
|
query = query.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(pkName)), id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,11 +845,42 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Standard batch update without nested relations
|
// Standard batch update without nested relations
|
||||||
|
pkName := reflection.GetPrimaryKeyName(model)
|
||||||
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
for _, item := range updates {
|
for _, item := range updates {
|
||||||
if itemID, ok := item["id"]; ok {
|
if itemID, ok := item["id"]; ok {
|
||||||
|
// First, read the existing record
|
||||||
|
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
||||||
|
selectQuery := tx.NewSelect().Model(existingRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
||||||
|
if err := selectQuery.ScanModel(ctx); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
continue // Skip if record not found
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to fetch existing record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
txQuery := tx.NewUpdate().Table(tableName).SetMap(item).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), itemID)
|
// Convert existing record to map
|
||||||
|
existingMap := make(map[string]interface{})
|
||||||
|
jsonData, err := json.Marshal(existingRecord)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal existing record: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(jsonData, &existingMap); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal existing record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge only non-null and non-empty values
|
||||||
|
for key, newValue := range item {
|
||||||
|
if newValue == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strVal, ok := newValue.(string); ok && strVal == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existingMap[key] = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
txQuery := tx.NewUpdate().Table(tableName).SetMap(existingMap).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
||||||
if _, err := txQuery.Exec(ctx); err != nil {
|
if _, err := txQuery.Exec(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -857,13 +951,44 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Standard batch update without nested relations
|
// Standard batch update without nested relations
|
||||||
|
pkName := reflection.GetPrimaryKeyName(model)
|
||||||
list := make([]interface{}, 0)
|
list := make([]interface{}, 0)
|
||||||
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
for _, item := range updates {
|
for _, item := range updates {
|
||||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||||
if itemID, ok := itemMap["id"]; ok {
|
if itemID, ok := itemMap["id"]; ok {
|
||||||
|
// First, read the existing record
|
||||||
|
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
||||||
|
selectQuery := tx.NewSelect().Model(existingRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
||||||
|
if err := selectQuery.ScanModel(ctx); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
continue // Skip if record not found
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to fetch existing record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
txQuery := tx.NewUpdate().Table(tableName).SetMap(itemMap).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(reflection.GetPrimaryKeyName(model))), itemID)
|
// Convert existing record to map
|
||||||
|
existingMap := make(map[string]interface{})
|
||||||
|
jsonData, err := json.Marshal(existingRecord)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal existing record: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(jsonData, &existingMap); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal existing record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge only non-null and non-empty values
|
||||||
|
for key, newValue := range itemMap {
|
||||||
|
if newValue == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strVal, ok := newValue.(string); ok && strVal == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existingMap[key] = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
txQuery := tx.NewUpdate().Table(tableName).SetMap(existingMap).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
||||||
if _, err := txQuery.Exec(ctx); err != nil {
|
if _, err := txQuery.Exec(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1239,6 +1239,26 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
|||||||
// Create temporary nested processor with transaction
|
// Create temporary nested processor with transaction
|
||||||
txNestedProcessor := common.NewNestedCUDProcessor(tx, h.registry, h)
|
txNestedProcessor := common.NewNestedCUDProcessor(tx, h.registry, h)
|
||||||
|
|
||||||
|
// First, read the existing record from the database
|
||||||
|
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
||||||
|
selectQuery := tx.NewSelect().Model(existingRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
|
||||||
|
if err := selectQuery.ScanModel(ctx); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return fmt.Errorf("record not found with ID: %v", targetID)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to fetch existing record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert existing record to map
|
||||||
|
existingMap := make(map[string]interface{})
|
||||||
|
jsonData, err := json.Marshal(existingRecord)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal existing record: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(jsonData, &existingMap); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal existing record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Extract nested relations if present (but don't process them yet)
|
// Extract nested relations if present (but don't process them yet)
|
||||||
var nestedRelations map[string]interface{}
|
var nestedRelations map[string]interface{}
|
||||||
if h.shouldUseNestedProcessor(dataMap, model) {
|
if h.shouldUseNestedProcessor(dataMap, model) {
|
||||||
@@ -1251,15 +1271,30 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
|||||||
nestedRelations = relations
|
nestedRelations = relations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge only non-null and non-empty values from the incoming request into the existing record
|
||||||
|
for key, newValue := range dataMap {
|
||||||
|
// Skip if the value is nil
|
||||||
|
if newValue == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if the value is an empty string
|
||||||
|
if strVal, ok := newValue.(string); ok && strVal == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the existing map with the new value
|
||||||
|
existingMap[key] = newValue
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure ID is in the data map for the update
|
// Ensure ID is in the data map for the update
|
||||||
dataMap[pkName] = targetID
|
existingMap[pkName] = targetID
|
||||||
|
dataMap = existingMap
|
||||||
|
|
||||||
// Populate model instance from dataMap to preserve custom types (like SqlJSONB)
|
// Populate model instance from dataMap to preserve custom types (like SqlJSONB)
|
||||||
// Get the type of the model, handling both pointer and non-pointer types
|
// Get the type of the model, handling both pointer and non-pointer types
|
||||||
modelType := reflect.TypeOf(model)
|
modelType := reflect.TypeOf(model)
|
||||||
if modelType.Kind() == reflect.Ptr {
|
modelType = reflection.GetPointerElement(modelType)
|
||||||
modelType = modelType.Elem()
|
|
||||||
}
|
|
||||||
modelInstance := reflect.New(modelType).Interface()
|
modelInstance := reflect.New(modelType).Interface()
|
||||||
if err := reflection.MapToStruct(dataMap, modelInstance); err != nil {
|
if err := reflection.MapToStruct(dataMap, modelInstance); err != nil {
|
||||||
return fmt.Errorf("failed to populate model from data: %w", err)
|
return fmt.Errorf("failed to populate model from data: %w", err)
|
||||||
@@ -1297,7 +1332,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
|||||||
|
|
||||||
// Fetch the updated record to return the new values
|
// Fetch the updated record to return the new values
|
||||||
modelValue := reflect.New(reflect.TypeOf(model)).Interface()
|
modelValue := reflect.New(reflect.TypeOf(model)).Interface()
|
||||||
selectQuery := tx.NewSelect().Model(modelValue).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
|
selectQuery = tx.NewSelect().Model(modelValue).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
|
||||||
if err := selectQuery.ScanModel(ctx); err != nil {
|
if err := selectQuery.ScanModel(ctx); err != nil {
|
||||||
return fmt.Errorf("failed to fetch updated record: %w", err)
|
return fmt.Errorf("failed to fetch updated record: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1563,9 +1598,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
|
|||||||
|
|
||||||
// First, fetch the record that will be deleted
|
// First, fetch the record that will be deleted
|
||||||
modelType := reflect.TypeOf(model)
|
modelType := reflect.TypeOf(model)
|
||||||
if modelType.Kind() == reflect.Ptr {
|
modelType = reflection.GetPointerElement(modelType)
|
||||||
modelType = modelType.Elem()
|
|
||||||
}
|
|
||||||
recordToDelete := reflect.New(modelType).Interface()
|
recordToDelete := reflect.New(modelType).Interface()
|
||||||
|
|
||||||
selectQuery := h.db.NewSelect().Model(recordToDelete).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
selectQuery := h.db.NewSelect().Model(recordToDelete).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||||
|
|||||||
Reference in New Issue
Block a user