mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-07-26 12:17:38 +00:00
52d3dca1fa
Tests / Unit Tests (push) Failing after 11s
Tests / Integration Tests (push) Failing after 15s
Build , Vet Test, and Lint / Build (push) Successful in 1m45s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 2m0s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 2m0s
Build , Vet Test, and Lint / Lint Code (push) Successful in 2m4s
Adds a BeforeOp HookType across resolvespec, restheadspec, websocketspec, mqttspec, and funcspec that fires before every SQL operation (read, create, update, delete, scan/query) via a new ExecuteBeforeOp helper. Also closes a row-level-security gap: resolvespec registered a BeforeScan hook for ApplyRowSecurity but never fired it, and websocketspec/mqttspec had no BeforeScan hook point at all, so row security was never applied to their queries. BeforeScan now fires right before the actual scan in all three, with the (possibly hook-modified) query used for execution.
180 lines
5.8 KiB
Go
180 lines
5.8 KiB
Go
package funcspec
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
|
"github.com/bitechdev/ResolveSpec/pkg/security"
|
|
)
|
|
|
|
// HookType defines the type of hook to execute
|
|
type HookType string
|
|
|
|
const (
|
|
// Query operation hooks (for SqlQuery - single record)
|
|
BeforeQuery HookType = "before_query"
|
|
AfterQuery HookType = "after_query"
|
|
|
|
// Query list operation hooks (for SqlQueryList - multiple records)
|
|
BeforeQueryList HookType = "before_query_list"
|
|
AfterQueryList HookType = "after_query_list"
|
|
|
|
// SQL execution hooks (just before SQL is executed)
|
|
BeforeSQLExec HookType = "before_sql_exec"
|
|
AfterSQLExec HookType = "after_sql_exec"
|
|
|
|
// Response hooks (before response is sent)
|
|
BeforeResponse HookType = "before_response"
|
|
|
|
// BeforeOp fires immediately before every SQL operation (query, query list, SQL exec).
|
|
// It 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
|
|
Request *http.Request
|
|
Writer http.ResponseWriter
|
|
|
|
// SQL query and variables
|
|
SQLQuery string // The SQL query being executed (can be modified by hooks)
|
|
Variables map[string]interface{} // Variables extracted from request
|
|
InputVars []string // Input variable placeholders found in query
|
|
MetaInfo map[string]interface{} // Metadata about the request
|
|
PropQry map[string]string // Property query parameters
|
|
|
|
// User context
|
|
UserContext *security.UserContext
|
|
|
|
// 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
|
|
|
|
// Pagination and filtering (for list queries)
|
|
SortColumns string
|
|
Limit int
|
|
Offset int
|
|
|
|
// Results
|
|
Result interface{} // Query result (single record or list)
|
|
Total int64 // Total count (for list queries)
|
|
Error error // Error if operation failed
|
|
ComplexAPI bool // Whether complex API response format is requested
|
|
NoCount bool // Whether count query should be skipped
|
|
BlankParams bool // Whether blank parameters should be removed
|
|
AllowFilter bool // Whether filtering is allowed
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// NewHookRegistry creates a new hook registry
|
|
func NewHookRegistry() *HookRegistry {
|
|
return &HookRegistry{
|
|
hooks: make(map[HookType][]HookFunc),
|
|
}
|
|
}
|
|
|
|
// Register adds a new hook for the specified hook type
|
|
func (r *HookRegistry) Register(hookType HookType, hook HookFunc) {
|
|
if r.hooks == nil {
|
|
r.hooks = make(map[HookType][]HookFunc)
|
|
}
|
|
r.hooks[hookType] = append(r.hooks[hookType], hook)
|
|
logger.Info("Registered funcspec 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 {
|
|
hooks, exists := r.hooks[hookType]
|
|
if !exists || len(hooks) == 0 {
|
|
return nil
|
|
}
|
|
|
|
logger.Debug("Executing %d funcspec hook(s) for %s", len(hooks), hookType)
|
|
|
|
for i, hook := range hooks {
|
|
if err := hook(ctx); err != nil {
|
|
logger.Error("Funcspec 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("Funcspec hook %d for %s requested abort: %s", i+1, hookType, ctx.AbortMessage)
|
|
return fmt.Errorf("operation aborted by hook: %s", ctx.AbortMessage)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ExecuteBeforeOp executes the BeforeOp hook followed by the given SQL-operation hook
|
|
// (BeforeQuery, BeforeQueryList, or BeforeSQLExec). 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) {
|
|
delete(r.hooks, hookType)
|
|
logger.Info("Cleared all funcspec hooks for %s", hookType)
|
|
}
|
|
|
|
// ClearAll removes all registered hooks
|
|
func (r *HookRegistry) ClearAll() {
|
|
r.hooks = make(map[HookType][]HookFunc)
|
|
logger.Info("Cleared all funcspec hooks")
|
|
}
|
|
|
|
// Count returns the number of hooks registered for a specific type
|
|
func (r *HookRegistry) Count(hookType HookType) int {
|
|
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 {
|
|
types := make([]HookType, 0, len(r.hooks))
|
|
for hookType := range r.hooks {
|
|
types = append(types, hookType)
|
|
}
|
|
return types
|
|
}
|