mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-07-30 14:17:39 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 873e8925d4 | |||
| b23916048a |
+55
-47
@@ -1056,47 +1056,18 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
// Get the primary key name
|
// Get the primary key name
|
||||||
pkName := reflection.GetPrimaryKeyName(model)
|
pkName := reflection.GetPrimaryKeyName(model)
|
||||||
|
|
||||||
|
if targetID == nil {
|
||||||
|
logger.Error("Update request for %s.%s has no ID (not in URL, request ID, or data)", schema, entity)
|
||||||
|
h.sendError(w, http.StatusBadRequest, "missing_id", "No ID provided for update", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Wrap in transaction to ensure BeforeUpdate hook is inside transaction
|
// Wrap in transaction to ensure BeforeUpdate hook is inside transaction
|
||||||
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
|
// Execute BeforeUpdate hooks inside transaction, before any queries run.
|
||||||
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
// BeforeUpdate hooks may set session-scoped RLS GUCs (via SET LOCAL);
|
||||||
selectQuery := tx.NewSelect().Model(existingRecord).Column(reflection.GetSQLModelColumns(model)...)
|
// they must run before the existence-check select so that select is
|
||||||
|
// also subject to RLS on this connection/transaction.
|
||||||
// Apply conditions to select
|
|
||||||
if urlID != "" {
|
|
||||||
logger.Debug("Updating by URL ID: %s", urlID)
|
|
||||||
selectQuery = selectQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), urlID)
|
|
||||||
} else if reqID != nil {
|
|
||||||
switch id := reqID.(type) {
|
|
||||||
case string:
|
|
||||||
logger.Debug("Updating by request ID: %s", id)
|
|
||||||
selectQuery = selectQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
|
||||||
case []string:
|
|
||||||
if len(id) > 0 {
|
|
||||||
logger.Debug("Updating by multiple IDs: %v", id)
|
|
||||||
selectQuery = selectQuery.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(pkName)), id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := selectQuery.ScanModel(ctx); err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return fmt.Errorf("no records found to update")
|
|
||||||
}
|
|
||||||
return fmt.Errorf("error fetching 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("error marshaling existing record: %w", err)
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(jsonData, &existingMap); err != nil {
|
|
||||||
return fmt.Errorf("error unmarshaling existing record: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute BeforeUpdate hooks inside transaction
|
|
||||||
hookCtx := &HookContext{
|
hookCtx := &HookContext{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Handler: h,
|
Handler: h,
|
||||||
@@ -1119,6 +1090,43 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
updates = modifiedData
|
updates = modifiedData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now read the existing record from the database
|
||||||
|
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
||||||
|
selectQuery := tx.NewSelect().Model(existingRecord).Column(reflection.GetSQLModelColumns(model)...)
|
||||||
|
|
||||||
|
// Apply conditions to select, based on the resolved target ID
|
||||||
|
// (URL ID, request ID, or the "id" field embedded in the data payload).
|
||||||
|
switch id := targetID.(type) {
|
||||||
|
case string:
|
||||||
|
logger.Debug("Updating by ID: %s", id)
|
||||||
|
selectQuery = selectQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||||
|
case []string:
|
||||||
|
if len(id) > 0 {
|
||||||
|
logger.Debug("Updating by multiple IDs: %v", id)
|
||||||
|
selectQuery = selectQuery.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(pkName)), id)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
logger.Debug("Updating by ID: %v", id)
|
||||||
|
selectQuery = selectQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := selectQuery.ScanModel(ctx); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return fmt.Errorf("no records found to update")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("error fetching 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("error marshaling existing record: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(jsonData, &existingMap); err != nil {
|
||||||
|
return fmt.Errorf("error unmarshaling existing record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Merge only non-null and non-empty values from the incoming request into the existing record
|
// Merge only non-null and non-empty values from the incoming request into the existing record
|
||||||
for key, newValue := range updates {
|
for key, newValue := range updates {
|
||||||
// Skip if the value is nil
|
// Skip if the value is nil
|
||||||
@@ -1138,16 +1146,16 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
// Build update query with merged data
|
// Build update query with merged data
|
||||||
query := tx.NewUpdate().Table(tableName).SetMap(existingMap)
|
query := tx.NewUpdate().Table(tableName).SetMap(existingMap)
|
||||||
|
|
||||||
// Apply conditions
|
// Apply conditions, using the same target ID resolved for the select above
|
||||||
if urlID != "" {
|
switch id := targetID.(type) {
|
||||||
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), urlID)
|
case string:
|
||||||
} else if reqID != nil {
|
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||||
switch id := reqID.(type) {
|
case []string:
|
||||||
case string:
|
if len(id) > 0 {
|
||||||
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
|
||||||
case []string:
|
|
||||||
query = query.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(pkName)), id)
|
query = query.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(pkName)), id)
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := query.Exec(ctx)
|
result, err := query.Exec(ctx)
|
||||||
|
|||||||
+28
-25
@@ -1405,7 +1405,34 @@ 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
|
// Execute BeforeUpdate hooks inside transaction, before any queries run.
|
||||||
|
// BeforeUpdate hooks may set session-scoped RLS GUCs (via SET LOCAL);
|
||||||
|
// they must run before the existence-check select so that select is
|
||||||
|
// also subject to RLS on this connection/transaction.
|
||||||
|
hookCtx = &HookContext{
|
||||||
|
Context: ctx,
|
||||||
|
Handler: h,
|
||||||
|
Schema: schema,
|
||||||
|
Entity: entity,
|
||||||
|
TableName: tableName,
|
||||||
|
Tx: tx,
|
||||||
|
Model: model,
|
||||||
|
Options: options,
|
||||||
|
ID: id,
|
||||||
|
Data: dataMap,
|
||||||
|
Writer: w,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
|
||||||
|
return fmt.Errorf("BeforeUpdate hook failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use potentially modified data from hook context
|
||||||
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
|
dataMap = modifiedData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now 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).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
|
selectQuery := tx.NewSelect().Model(existingRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
|
||||||
if err := selectQuery.ScanModel(ctx); err != nil {
|
if err := selectQuery.ScanModel(ctx); err != nil {
|
||||||
@@ -1437,30 +1464,6 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
|||||||
nestedRelations = relations
|
nestedRelations = relations
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute BeforeUpdate hooks inside transaction
|
|
||||||
hookCtx = &HookContext{
|
|
||||||
Context: ctx,
|
|
||||||
Handler: h,
|
|
||||||
Schema: schema,
|
|
||||||
Entity: entity,
|
|
||||||
TableName: tableName,
|
|
||||||
Tx: tx,
|
|
||||||
Model: model,
|
|
||||||
Options: options,
|
|
||||||
ID: id,
|
|
||||||
Data: dataMap,
|
|
||||||
Writer: w,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
|
|
||||||
return fmt.Errorf("BeforeUpdate hook failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use potentially modified data from hook context
|
|
||||||
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
|
||||||
dataMap = modifiedData
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge only non-null and non-empty values from the incoming request into the existing record
|
// Merge only non-null and non-empty values from the incoming request into the existing record
|
||||||
for key, newValue := range dataMap {
|
for key, newValue := range dataMap {
|
||||||
// Skip if the value is nil
|
// Skip if the value is nil
|
||||||
|
|||||||
Reference in New Issue
Block a user