Compare commits

..

3 Commits

Author SHA1 Message Date
Hein 3b6e5c75be fix(handler): update foreign key field resolution logic
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Waiting to run
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Waiting to run
Build , Vet Test, and Lint / Lint Code (push) Waiting to run
Build , Vet Test, and Lint / Build (push) Waiting to run
Tests / Unit Tests (push) Waiting to run
Tests / Integration Tests (push) Waiting to run
* Adjust foreign key field name selection for has-many/has-one relationships
* Improve logging to clarify foreign key and child field usage
2026-06-07 14:20:55 +02:00
Hein 549ccb8468 fix(handler): fetch updated records after transaction commits
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Has been cancelled
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Has been cancelled
Build , Vet Test, and Lint / Lint Code (push) Has been cancelled
Build , Vet Test, and Lint / Build (push) Has been cancelled
Tests / Unit Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
* Update selection queries to use model columns
* Ensure updated records are fetched and returned in responses
2026-06-05 11:12:04 +02:00
Hein 1af9c76337 fix(handler): fetch updated record after transaction commits
Tests / Unit Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Has been cancelled
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Has been cancelled
Build , Vet Test, and Lint / Lint Code (push) Has been cancelled
Build , Vet Test, and Lint / Build (push) Has been cancelled
2026-06-04 18:23:18 +02:00
3 changed files with 92 additions and 30 deletions
+10 -6
View File
@@ -471,13 +471,17 @@ func (p *NestedCUDProcessor) processChildRelations(
// Priority: Use foreign key field name if specified // Priority: Use foreign key field name if specified
var foreignKeyFieldName string var foreignKeyFieldName string
if relInfo.ForeignKey != "" { if relInfo.ForeignKey != "" {
// Get the JSON name for the foreign key field in the child model // For has-many/has-one: join:parentCol=childCol
foreignKeyFieldName = reflection.GetJSONNameForField(relatedModelType, relInfo.ForeignKey) // ForeignKey = parent side, References = child side (where we actually set the value)
if foreignKeyFieldName == "" { childField := relInfo.ForeignKey
// Fallback to lowercase field name if (relInfo.RelationType == "hasMany" || relInfo.RelationType == "hasOne") && relInfo.References != "" {
foreignKeyFieldName = strings.ToLower(relInfo.ForeignKey) childField = relInfo.References
} }
logger.Debug("Using foreign key field for direct assignment: %s (from FK %s)", foreignKeyFieldName, relInfo.ForeignKey) foreignKeyFieldName = reflection.GetJSONNameForField(relatedModelType, childField)
if foreignKeyFieldName == "" {
foreignKeyFieldName = strings.ToLower(childField)
}
logger.Debug("Using foreign key field for direct assignment: %s (from FK %s -> child %s)", foreignKeyFieldName, relInfo.ForeignKey, childField)
} }
// Get the primary key name for the child model to avoid overwriting it in recursive relationships // Get the primary key name for the child model to avoid overwriting it in recursive relationships
+63 -8
View File
@@ -836,7 +836,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
err := h.db.RunInTransaction(ctx, func(tx common.Database) error { err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
// First, read the existing record from the database // First, read the existing record from the database
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface() existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
selectQuery := tx.NewSelect().Model(existingRecord).Column("*") selectQuery := tx.NewSelect().Model(existingRecord).Column(reflection.GetSQLModelColumns(model)...)
// Apply conditions to select // Apply conditions to select
if urlID != "" { if urlID != "" {
@@ -955,13 +955,34 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
return return
} }
// Fetch the updated record after the transaction commits to capture any trigger changes
updatedRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
fetchQuery := h.db.NewSelect().Model(updatedRecord).Column(reflection.GetSQLModelColumns(model)...)
if urlID != "" {
fetchQuery = fetchQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), urlID)
} else if reqID != nil {
switch id := reqID.(type) {
case string:
fetchQuery = fetchQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
case []string:
if len(id) > 0 {
fetchQuery = fetchQuery.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(pkName)), id)
}
}
}
if err := fetchQuery.ScanModel(ctx); err != nil {
logger.Error("Failed to fetch updated record: %v", err)
h.sendError(w, http.StatusInternalServerError, "fetch_error", "Failed to fetch updated record", err)
return
}
logger.Info("Successfully updated record(s)") logger.Info("Successfully updated record(s)")
// Invalidate cache for this table // Invalidate cache for this table
cacheTags := buildCacheTags(schema, tableName) cacheTags := buildCacheTags(schema, tableName)
if err := invalidateCacheForTags(ctx, cacheTags); err != nil { if err := invalidateCacheForTags(ctx, cacheTags); err != nil {
logger.Warn("Failed to invalidate cache for table %s: %v", tableName, err) logger.Warn("Failed to invalidate cache for table %s: %v", tableName, err)
} }
h.sendResponse(w, data, nil) h.sendResponse(w, updatedRecord, nil)
case []map[string]interface{}: case []map[string]interface{}:
// Batch update with array of objects // Batch update with array of objects
@@ -1017,7 +1038,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
// First, read the existing record // First, read the existing record
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface() existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
selectQuery := tx.NewSelect().Model(existingRecord).Column("*").Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID) selectQuery := tx.NewSelect().Model(existingRecord).Column(reflection.GetSQLModelColumns(model)...).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
if err := selectQuery.ScanModel(ctx); err != nil { if err := selectQuery.ScanModel(ctx); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
continue // Skip if record not found continue // Skip if record not found
@@ -1089,13 +1110,29 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating records", err) h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating records", err)
return return
} }
logger.Info("Successfully updated %d records", len(updates))
// Fetch updated records after the transaction commits to capture any trigger changes
fetchedUpdates := make([]interface{}, 0, len(updates))
for _, item := range updates {
if itemID, ok := item["id"]; ok && itemID != nil {
fetchedRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
fetchQuery := h.db.NewSelect().Model(fetchedRecord).Column(reflection.GetSQLModelColumns(model)...).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
if err := fetchQuery.ScanModel(ctx); err != nil {
logger.Error("Failed to fetch updated record with ID %v: %v", itemID, err)
h.sendError(w, http.StatusInternalServerError, "fetch_error", "Failed to fetch updated record", err)
return
}
fetchedUpdates = append(fetchedUpdates, fetchedRecord)
}
}
logger.Info("Successfully updated %d records", len(fetchedUpdates))
// Invalidate cache for this table // Invalidate cache for this table
cacheTags := buildCacheTags(schema, tableName) cacheTags := buildCacheTags(schema, tableName)
if err := invalidateCacheForTags(ctx, cacheTags); err != nil { if err := invalidateCacheForTags(ctx, cacheTags); err != nil {
logger.Warn("Failed to invalidate cache for table %s: %v", tableName, err) logger.Warn("Failed to invalidate cache for table %s: %v", tableName, err)
} }
h.sendResponse(w, updates, nil) h.sendResponse(w, fetchedUpdates, nil)
case []interface{}: case []interface{}:
// Batch update with []interface{} // Batch update with []interface{}
@@ -1157,7 +1194,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
// First, read the existing record // First, read the existing record
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface() existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
selectQuery := tx.NewSelect().Model(existingRecord).Column("*").Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID) selectQuery := tx.NewSelect().Model(existingRecord).Column(reflection.GetSQLModelColumns(model)...).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
if err := selectQuery.ScanModel(ctx); err != nil { if err := selectQuery.ScanModel(ctx); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
continue // Skip if record not found continue // Skip if record not found
@@ -1232,13 +1269,31 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating records", err) h.sendError(w, http.StatusInternalServerError, "update_error", "Error updating records", err)
return return
} }
logger.Info("Successfully updated %d records", len(list))
// Fetch updated records after the transaction commits to capture any trigger changes
fetchedList := make([]interface{}, 0, len(list))
for _, item := range list {
if itemMap, ok := item.(map[string]interface{}); ok {
if itemID, ok := itemMap["id"]; ok && itemID != nil {
fetchedRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
fetchQuery := h.db.NewSelect().Model(fetchedRecord).Column(reflection.GetSQLModelColumns(model)...).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
if err := fetchQuery.ScanModel(ctx); err != nil {
logger.Error("Failed to fetch updated record with ID %v: %v", itemID, err)
h.sendError(w, http.StatusInternalServerError, "fetch_error", "Failed to fetch updated record", err)
return
}
fetchedList = append(fetchedList, fetchedRecord)
}
}
}
logger.Info("Successfully updated %d records", len(fetchedList))
// Invalidate cache for this table // Invalidate cache for this table
cacheTags := buildCacheTags(schema, tableName) cacheTags := buildCacheTags(schema, tableName)
if err := invalidateCacheForTags(ctx, cacheTags); err != nil { if err := invalidateCacheForTags(ctx, cacheTags); err != nil {
logger.Warn("Failed to invalidate cache for table %s: %v", tableName, err) logger.Warn("Failed to invalidate cache for table %s: %v", tableName, err)
} }
h.sendResponse(w, list, nil) h.sendResponse(w, fetchedList, nil)
default: default:
logger.Error("Invalid data type for update operation: %T", data) logger.Error("Invalid data type for update operation: %T", data)
+19 -16
View File
@@ -1480,18 +1480,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
} }
} }
// Fetch the updated record to return the new values _ = result
modelValue := reflect.New(reflect.TypeOf(model)).Interface()
selectQuery = tx.NewSelect().Model(modelValue).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
if err := selectQuery.ScanModel(ctx); err != nil {
return fmt.Errorf("failed to fetch updated record: %w", err)
}
updatedRecord = modelValue
// Store result for hooks
hookCtx.Result = updatedRecord
_ = result // Keep result variable for potential future use
return nil return nil
}) })
@@ -1501,6 +1490,16 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
return return
} }
// Fetch the updated record after the transaction commits to capture any trigger changes
fetchedRecord := reflect.New(reflect.TypeOf(model)).Interface()
selectQuery := h.db.NewSelect().Model(fetchedRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
if err := selectQuery.ScanModel(ctx); err != nil {
logger.Error("Failed to fetch updated record: %v", err)
h.sendError(w, http.StatusInternalServerError, "fetch_error", "Failed to fetch updated record", err)
return
}
updatedRecord = fetchedRecord
// Merge the updated record with the original request data // Merge the updated record with the original request data
// This preserves extra keys from the request and updates values from the database // This preserves extra keys from the request and updates values from the database
mergedData := h.mergeRecordWithRequest(updatedRecord, dataMap) mergedData := h.mergeRecordWithRequest(updatedRecord, dataMap)
@@ -2012,11 +2011,15 @@ func (h *Handler) processChildRelationsForField(
// Priority: Use foreign key field name if specified, otherwise use parent's PK name // Priority: Use foreign key field name if specified, otherwise use parent's PK name
var foreignKeyFieldName string var foreignKeyFieldName string
if relInfo.ForeignKey != "" { if relInfo.ForeignKey != "" {
// Get the JSON name for the foreign key field in the child model // For has-many/has-one: join:parentCol=childCol
foreignKeyFieldName = reflection.GetJSONNameForField(relatedModelType, relInfo.ForeignKey) // ForeignKey = parent side, References = child side (where we actually set the value)
childField := relInfo.ForeignKey
if (relInfo.RelationType == "hasMany" || relInfo.RelationType == "hasOne") && relInfo.References != "" {
childField = relInfo.References
}
foreignKeyFieldName = reflection.GetJSONNameForField(relatedModelType, childField)
if foreignKeyFieldName == "" { if foreignKeyFieldName == "" {
// Fallback to lowercase field name foreignKeyFieldName = strings.ToLower(childField)
foreignKeyFieldName = strings.ToLower(relInfo.ForeignKey)
} }
} else { } else {
// Fallback: use parent's primary key name // Fallback: use parent's primary key name