Files
ResolveSpec/pkg/websocketspec/hooks.go
T
warkanum 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
feat(hooks): add BeforeOp hook and fix BeforeScan row-security gap
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.
2026-07-25 11:39:31 +02:00

235 lines
7.0 KiB
Go

package websocketspec
import (
"context"
"fmt"
"github.com/bitechdev/ResolveSpec/pkg/common"
)
// HookType represents the type of lifecycle hook
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"
// BeforeRead is called before a read operation
BeforeRead HookType = "before_read"
// AfterRead is called after a read operation
AfterRead HookType = "after_read"
// BeforeCreate is called before a create operation
BeforeCreate HookType = "before_create"
// AfterCreate is called after a create operation
AfterCreate HookType = "after_create"
// BeforeUpdate is called before an update operation
BeforeUpdate HookType = "before_update"
// AfterUpdate is called after an update operation
AfterUpdate HookType = "after_update"
// BeforeDelete is called before a delete operation
BeforeDelete HookType = "before_delete"
// AfterDelete is called after a delete operation
AfterDelete HookType = "after_delete"
// BeforeScan is called right before a read query is executed against the database,
// after all filters/sort/pagination have been applied. Use this for row-level
// security that needs to modify the query (stored in HookContext.Metadata["query"]).
BeforeScan HookType = "before_scan"
// BeforeSubscribe is called before creating a subscription
BeforeSubscribe HookType = "before_subscribe"
// AfterSubscribe is called after creating a subscription
AfterSubscribe HookType = "after_subscribe"
// BeforeUnsubscribe is called before removing a subscription
BeforeUnsubscribe HookType = "before_unsubscribe"
// AfterUnsubscribe is called after removing a subscription
AfterUnsubscribe HookType = "after_unsubscribe"
// BeforeConnect is called when a new connection is established
BeforeConnect HookType = "before_connect"
// AfterConnect is called after a connection is established
AfterConnect HookType = "after_connect"
// BeforeDisconnect is called before a connection is closed
BeforeDisconnect HookType = "before_disconnect"
// AfterDisconnect is called after a connection is closed
AfterDisconnect HookType = "after_disconnect"
// BeforeOp fires immediately before every SQL operation (read, create, update, delete).
// 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 context information for hook execution
type HookContext struct {
// Context is the request context
Context context.Context
// Handler provides access to the handler, database, and registry
Handler *Handler
// Connection is the WebSocket connection
Connection *Connection
// Message is the original message
Message *Message
// Schema is the database schema
Schema string
// Entity is the table/model name
Entity string
// TableName is the actual database table name
TableName string
// Model is the registered model instance
Model interface{}
// ModelPtr is a pointer to the model for queries
ModelPtr interface{}
// Options contains the parsed request options
Options *common.RequestOptions
// Operation being dispatched (e.g. "read", "create", "update", "delete")
Operation string
// ID is the record ID for single-record operations
ID string
// Data is the request data (for create/update operations)
Data interface{}
// Result is the operation result (for after hooks)
Result interface{}
// Subscription is the subscription being created/removed
Subscription *Subscription
// Error is any error that occurred (for after hooks)
Error error
// 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
Tx common.Database
// Metadata is additional context data
Metadata map[string]interface{}
}
// HookFunc is a function that processes a hook
type HookFunc func(*HookContext) error
// HookRegistry manages lifecycle 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 registers a hook function for a specific hook type
func (hr *HookRegistry) Register(hookType HookType, fn HookFunc) {
hr.hooks[hookType] = append(hr.hooks[hookType], fn)
}
// RegisterBefore registers a hook that runs before an operation
// Convenience method for BeforeRead, BeforeCreate, BeforeUpdate, BeforeDelete
func (hr *HookRegistry) RegisterBefore(operation OperationType, fn HookFunc) {
switch operation {
case OperationRead:
hr.Register(BeforeRead, fn)
case OperationCreate:
hr.Register(BeforeCreate, fn)
case OperationUpdate:
hr.Register(BeforeUpdate, fn)
case OperationDelete:
hr.Register(BeforeDelete, fn)
case OperationSubscribe:
hr.Register(BeforeSubscribe, fn)
case OperationUnsubscribe:
hr.Register(BeforeUnsubscribe, fn)
}
}
// RegisterAfter registers a hook that runs after an operation
// Convenience method for AfterRead, AfterCreate, AfterUpdate, AfterDelete
func (hr *HookRegistry) RegisterAfter(operation OperationType, fn HookFunc) {
switch operation {
case OperationRead:
hr.Register(AfterRead, fn)
case OperationCreate:
hr.Register(AfterCreate, fn)
case OperationUpdate:
hr.Register(AfterUpdate, fn)
case OperationDelete:
hr.Register(AfterDelete, fn)
case OperationSubscribe:
hr.Register(AfterSubscribe, fn)
case OperationUnsubscribe:
hr.Register(AfterUnsubscribe, fn)
}
}
// Execute runs all hooks for a specific type
func (hr *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
hooks, exists := hr.hooks[hookType]
if !exists {
return nil
}
for _, hook := range hooks {
if err := hook(ctx); err != nil {
return err
}
// Check if hook requested abort
if ctx.Abort {
return fmt.Errorf("operation aborted by hook: %s", ctx.AbortMessage)
}
}
return nil
}
// ExecuteBeforeOp executes the BeforeOp hook followed by the given SQL-operation hook
// (BeforeRead, BeforeCreate, BeforeUpdate, or BeforeDelete). BeforeOp always runs first
// so it can observe/veto every SQL operation regardless of type.
func (hr *HookRegistry) ExecuteBeforeOp(hookType HookType, ctx *HookContext) error {
if err := hr.Execute(BeforeOp, ctx); err != nil {
return err
}
return hr.Execute(hookType, ctx)
}
// HasHooks checks if any hooks are registered for a hook type
func (hr *HookRegistry) HasHooks(hookType HookType) bool {
hooks, exists := hr.hooks[hookType]
return exists && len(hooks) > 0
}
// Clear removes all hooks of a specific type
func (hr *HookRegistry) Clear(hookType HookType) {
delete(hr.hooks, hookType)
}
// ClearAll removes all registered hooks
func (hr *HookRegistry) ClearAll() {
hr.hooks = make(map[HookType][]HookFunc)
}