From b23916048af29fa8a6c03b086336baae5c54142e Mon Sep 17 00:00:00 2001 From: Hein Date: Fri, 24 Jul 2026 15:56:23 +0200 Subject: [PATCH] fix: ordering in before hook --- pkg/resolvespec/handler.go | 51 ++++++++++++++++++----------------- pkg/restheadspec/handler.go | 53 ++++++++++++++++++++----------------- 2 files changed, 55 insertions(+), 49 deletions(-) diff --git a/pkg/resolvespec/handler.go b/pkg/resolvespec/handler.go index 6cbf696..3cd439b 100644 --- a/pkg/resolvespec/handler.go +++ b/pkg/resolvespec/handler.go @@ -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 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() 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) } - // 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 for key, newValue := range updates { // Skip if the value is nil diff --git a/pkg/restheadspec/handler.go b/pkg/restheadspec/handler.go index 8b6c6e3..331e306 100644 --- a/pkg/restheadspec/handler.go +++ b/pkg/restheadspec/handler.go @@ -1405,7 +1405,34 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id // Create temporary nested processor with transaction 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() selectQuery := tx.NewSelect().Model(existingRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID) if err := selectQuery.ScanModel(ctx); err != nil { @@ -1437,30 +1464,6 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id 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 for key, newValue := range dataMap { // Skip if the value is nil