mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-08-04 16:47:39 +00:00
16cc7d350e
Tests / Unit Tests (push) Failing after 10s
Tests / Integration Tests (push) Failing after 13s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 1m5s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 1m3s
Build , Vet Test, and Lint / Build (push) Successful in 1m7s
Build , Vet Test, and Lint / Lint Code (push) Successful in 1m48s
* Add tryLock and tryRLock methods to manage mutex access * Update Register, Clear, and Execute methods to handle locked state * Log errors when registry operations fail due to locking
256 lines
7.3 KiB
Go
256 lines
7.3 KiB
Go
package restheadspec
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
|
)
|
|
|
|
// HookType defines the type of hook to execute
|
|
type HookType string
|
|
|
|
const (
|
|
// BeforeHandle fires after model resolution, before operation dispatch.
|
|
// Use this for auth checks that need model rules and user context simultaneously.
|
|
BeforeHandle HookType = "before_handle"
|
|
|
|
// Read operation hooks
|
|
BeforeRead HookType = "before_read"
|
|
AfterRead HookType = "after_read"
|
|
|
|
// Create operation hooks
|
|
BeforeCreate HookType = "before_create"
|
|
AfterCreate HookType = "after_create"
|
|
|
|
// Update operation hooks
|
|
BeforeUpdate HookType = "before_update"
|
|
AfterUpdate HookType = "after_update"
|
|
|
|
// Delete operation hooks
|
|
BeforeDelete HookType = "before_delete"
|
|
AfterDelete HookType = "after_delete"
|
|
|
|
// Scan/Execute operation hooks
|
|
BeforeScan HookType = "before_scan"
|
|
|
|
// BeforeOp fires immediately before every SQL operation (read, create, update, delete, scan).
|
|
// Unlike BeforeHandle, which fires once before operation dispatch, BeforeOp fires at each
|
|
// individual SQL-operation hook point, so it runs once per statement executed.
|
|
BeforeOp HookType = "before_op"
|
|
)
|
|
|
|
// HookContext contains all the data available to a hook
|
|
type HookContext struct {
|
|
Context context.Context
|
|
Handler *Handler // Reference to the handler for accessing database, registry, etc.
|
|
Schema string
|
|
Entity string
|
|
TableName string
|
|
Model interface{}
|
|
Options ExtendedRequestOptions
|
|
|
|
// Operation being dispatched (e.g. "read", "create", "update", "delete")
|
|
Operation string
|
|
|
|
// Operation-specific fields
|
|
ID string
|
|
Data interface{} // For create/update operations
|
|
Result interface{} // For after hooks
|
|
Error error // For after hooks
|
|
QueryFilter string // For read operations
|
|
|
|
// Query chain - allows hooks to modify the query before execution
|
|
// Can be SelectQuery, InsertQuery, UpdateQuery, or DeleteQuery
|
|
Query interface{}
|
|
|
|
// Response writer - allows hooks to modify response
|
|
Writer common.ResponseWriter
|
|
|
|
// Request - the original HTTP request
|
|
Request common.Request
|
|
|
|
// Allow hooks to abort the operation
|
|
Abort bool // If set to true, the operation will be aborted
|
|
AbortMessage string // Message to return if aborted
|
|
AbortCode int // HTTP status code if aborted
|
|
|
|
// 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
|
|
}
|
|
|
|
// HookFunc is the signature for hook functions
|
|
// It receives a HookContext and can modify it or return an error
|
|
// If an error is returned, the operation will be aborted
|
|
type HookFunc func(*HookContext) error
|
|
|
|
// HookRegistry manages all registered hooks
|
|
type HookRegistry struct {
|
|
hooks map[HookType][]HookFunc
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
// NewHookRegistry creates a new hook registry
|
|
func NewHookRegistry() *HookRegistry {
|
|
return &HookRegistry{
|
|
hooks: make(map[HookType][]HookFunc),
|
|
}
|
|
}
|
|
|
|
// hookLockRetryAttempts/hookLockRetryDelay bound how long the try-lock
|
|
// helpers below will spin before giving up, so a contended mutex can
|
|
// never hang a caller.
|
|
const (
|
|
hookLockRetryAttempts = 20
|
|
hookLockRetryDelay = 1 * time.Millisecond
|
|
)
|
|
|
|
// tryLock attempts to acquire the write lock, retrying briefly. Returns
|
|
// false if it could not be acquired within the bound.
|
|
func (r *HookRegistry) tryLock() bool {
|
|
for i := 0; i < hookLockRetryAttempts; i++ {
|
|
if r.mutex.TryLock() {
|
|
return true
|
|
}
|
|
time.Sleep(hookLockRetryDelay)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// tryRLock attempts to acquire the read lock, retrying briefly. Returns
|
|
// false if it could not be acquired within the bound.
|
|
func (r *HookRegistry) tryRLock() bool {
|
|
for i := 0; i < hookLockRetryAttempts; i++ {
|
|
if r.mutex.TryRLock() {
|
|
return true
|
|
}
|
|
time.Sleep(hookLockRetryDelay)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Register adds a new hook for the specified hook type
|
|
func (r *HookRegistry) Register(hookType HookType, hook HookFunc) {
|
|
if !r.tryLock() {
|
|
logger.Error("Failed to register hook for %s: registry locked", hookType)
|
|
return
|
|
}
|
|
defer r.mutex.Unlock()
|
|
|
|
if r.hooks == nil {
|
|
r.hooks = make(map[HookType][]HookFunc)
|
|
}
|
|
r.hooks[hookType] = append(r.hooks[hookType], hook)
|
|
logger.Info("Registered hook for %s (total: %d)", hookType, len(r.hooks[hookType]))
|
|
}
|
|
|
|
// RegisterMultiple registers a hook for multiple hook types
|
|
func (r *HookRegistry) RegisterMultiple(hookTypes []HookType, hook HookFunc) {
|
|
for _, hookType := range hookTypes {
|
|
r.Register(hookType, hook)
|
|
}
|
|
}
|
|
|
|
// Execute runs all hooks for the specified type in order
|
|
// If any hook returns an error, execution stops and the error is returned
|
|
func (r *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
|
|
if !r.tryRLock() {
|
|
return fmt.Errorf("hook execution failed: registry locked")
|
|
}
|
|
hooks := append([]HookFunc(nil), r.hooks[hookType]...)
|
|
r.mutex.RUnlock()
|
|
|
|
if len(hooks) == 0 {
|
|
// logger.Debug("No hooks registered for %s", hookType)
|
|
return nil
|
|
}
|
|
|
|
logger.Debug("Executing %d hook(s) for %s", len(hooks), hookType)
|
|
|
|
for i, hook := range hooks {
|
|
if err := hook(ctx); err != nil {
|
|
logger.Error("Hook %d for %s failed: %v", i+1, hookType, err)
|
|
return fmt.Errorf("hook execution failed: %w", err)
|
|
}
|
|
|
|
// Check if hook requested abort
|
|
if ctx.Abort {
|
|
logger.Warn("Hook %d for %s requested abort: %s", i+1, hookType, ctx.AbortMessage)
|
|
return fmt.Errorf("operation aborted by hook: %s", ctx.AbortMessage)
|
|
}
|
|
}
|
|
|
|
// logger.Debug("All hooks for %s executed successfully", hookType)
|
|
return nil
|
|
}
|
|
|
|
// ExecuteBeforeOp executes the BeforeOp hook followed by the given SQL-operation hook
|
|
// (BeforeRead, BeforeCreate, BeforeUpdate, BeforeDelete, or BeforeScan). BeforeOp always
|
|
// runs first so it can observe/veto every SQL operation regardless of type.
|
|
func (r *HookRegistry) ExecuteBeforeOp(hookType HookType, ctx *HookContext) error {
|
|
if err := r.Execute(BeforeOp, ctx); err != nil {
|
|
return err
|
|
}
|
|
return r.Execute(hookType, ctx)
|
|
}
|
|
|
|
// Clear removes all hooks for the specified type
|
|
func (r *HookRegistry) Clear(hookType HookType) {
|
|
if !r.tryLock() {
|
|
logger.Error("Failed to clear hooks for %s: registry locked", hookType)
|
|
return
|
|
}
|
|
defer r.mutex.Unlock()
|
|
|
|
delete(r.hooks, hookType)
|
|
logger.Info("Cleared all hooks for %s", hookType)
|
|
}
|
|
|
|
// ClearAll removes all registered hooks
|
|
func (r *HookRegistry) ClearAll() {
|
|
if !r.tryLock() {
|
|
logger.Error("Failed to clear all hooks: registry locked")
|
|
return
|
|
}
|
|
defer r.mutex.Unlock()
|
|
|
|
r.hooks = make(map[HookType][]HookFunc)
|
|
logger.Info("Cleared all hooks")
|
|
}
|
|
|
|
// Count returns the number of hooks registered for a specific type
|
|
func (r *HookRegistry) Count(hookType HookType) int {
|
|
if !r.tryRLock() {
|
|
return 0
|
|
}
|
|
defer r.mutex.RUnlock()
|
|
|
|
if hooks, exists := r.hooks[hookType]; exists {
|
|
return len(hooks)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// HasHooks returns true if there are any hooks registered for the specified type
|
|
func (r *HookRegistry) HasHooks(hookType HookType) bool {
|
|
return r.Count(hookType) > 0
|
|
}
|
|
|
|
// GetAllHookTypes returns all hook types that have registered hooks
|
|
func (r *HookRegistry) GetAllHookTypes() []HookType {
|
|
if !r.tryRLock() {
|
|
return nil
|
|
}
|
|
defer r.mutex.RUnlock()
|
|
|
|
types := make([]HookType, 0, len(r.hooks))
|
|
for hookType := range r.hooks {
|
|
types = append(types, hookType)
|
|
}
|
|
return types
|
|
}
|