fix: ordering in before hook

This commit is contained in:
Hein
2026-07-24 15:56:23 +02:00
parent 47708fc87a
commit b23916048a
2 changed files with 55 additions and 49 deletions
+27 -24
View File
@@ -1058,7 +1058,33 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
// 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.
// 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,
Model: model,
Options: options,
ID: urlID,
Data: updates,
Writer: w,
Tx: tx,
}
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 {
updates = 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).Column(reflection.GetSQLModelColumns(model)...) selectQuery := tx.NewSelect().Model(existingRecord).Column(reflection.GetSQLModelColumns(model)...)
@@ -1096,29 +1122,6 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
return fmt.Errorf("error unmarshaling existing record: %w", err) return fmt.Errorf("error unmarshaling existing record: %w", err)
} }
// Execute BeforeUpdate hooks inside transaction
hookCtx := &HookContext{
Context: ctx,
Handler: h,
Schema: schema,
Entity: entity,
Model: model,
Options: options,
ID: urlID,
Data: updates,
Writer: w,
Tx: tx,
}
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 {
updates = 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 updates { for key, newValue := range updates {
// Skip if the value is nil // Skip if the value is nil
+28 -25
View File
@@ -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