Documentation
¶
Index ¶
- func SetObserver(fn func(Firing))
- type Firing
- type GetPayload
- type HookFunc
- type HookRegistry
- func (hr *HookRegistry) ExecuteHooks(ctx context.Context, hookType HookType, data any) error
- func (hr *HookRegistry) HooksFor(hookType HookType) []HookFunc
- func (hr *HookRegistry) Label() string
- func (hr *HookRegistry) RegisterHook(hookType HookType, fn HookFunc)
- func (hr *HookRegistry) SetLabel(label string)
- type HookType
- type ListPayload
- type PanicError
- type WhereClause
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func SetObserver ¶ added in v0.59.0
func SetObserver(fn func(Firing))
SetObserver installs a callback fired whenever a registered hook runs. Pass nil to clear. Intended for test tooling, the framework's semantic coverage recorder is the first consumer.
The callback runs inline on the request path, so it must be cheap, must not block, and must tolerate concurrent calls.
Types ¶
type Firing ¶ added in v0.59.0
type Firing struct {
// Entity is the registry's label, the entity name the framework
// created it for. Empty for a registry nobody labelled.
Entity string
// Type is the lifecycle point.
Type HookType
}
Firing is one registered hook that actually ran.
The distinction that matters is "ran" versus "was looked up". ExecuteHooks is called on every CRUD operation whether or not anything is registered, so recording the call would report full coverage for an app with no hooks at all. Only a hook with a body behind it counts.
type GetPayload ¶
type GetPayload struct {
Request *http.Request
ID string
Where []WhereClause
Result map[string]any
}
GetPayload is the data argument passed to BeforeGet and AfterGet hooks.
BeforeGet: Request and ID are populated, Where starts empty, Result is nil. Hooks call AddWhere() to scope the lookup (mismatches → 404). The record writes run the same chain, inside their transaction and before they write: PUT, PATCH, DELETE, a transition, _batch update and delete items, and in process under crud.WithReadHooks, so a row the clauses exclude answers those writes the same 404 and is left untouched.
AfterGet: Request, ID, and Result are populated; Where is no longer applied. Hooks may mutate Result in place to redact / transform. The redaction warning on ListPayload applies here too: mark a masked field NoQuery, or the List surface still filters and sorts on the stored value.
func (*GetPayload) AddWhere ¶
func (p *GetPayload) AddWhere(sql string, args ...any)
AddWhere appends a parameterised WHERE clause. Use $1, $2, … placeholders.
type HookFunc ¶
HookFunc is the signature for a lifecycle hook. The data argument varies by hook type (e.g. map[string]any for create/update, string ID for delete). An error stops the hook chain and fails the operation. CRUD write hooks, including After* hooks, run before commit; their errors roll back the write transaction. A read hook error fails the read rather than merely being logged.
type HookRegistry ¶
type HookRegistry struct {
// contains filtered or unexported fields
}
HookRegistry stores lifecycle hooks grouped by hook type.
Registration is normally a setup-time activity, but two things read a registry on the request path, the CRUD handler's own hook lookups, and ?include= resolving a CHILD entity's registry, while kiln's build-mode runtime registers hooks against a live server. An unguarded map would make that pairing a concurrent read/write, which is an unrecoverable runtime throw rather than a panic a hook recover() could catch.
func NewHookRegistry ¶
func NewHookRegistry() *HookRegistry
NewHookRegistry creates an empty HookRegistry.
func (*HookRegistry) ExecuteHooks ¶
ExecuteHooks runs all registered hooks for the given type in registration order. It stops on the first error and returns it. A panic inside a hook is caught and surfaced as an error, without recovery a single buggy or third-party hook would tear down the entire request goroutine.
func (*HookRegistry) HooksFor ¶
func (hr *HookRegistry) HooksFor(hookType HookType) []HookFunc
HooksFor returns a copy of the hooks registered for the given type (for inspection/testing).
func (*HookRegistry) Label ¶ added in v0.59.0
func (hr *HookRegistry) Label() string
Label returns the registry's entity name, "" when unlabelled.
func (*HookRegistry) RegisterHook ¶
func (hr *HookRegistry) RegisterHook(hookType HookType, fn HookFunc)
RegisterHook appends a hook function for the given hook type. Hooks execute in registration order.
func (*HookRegistry) SetLabel ¶ added in v0.59.0
func (hr *HookRegistry) SetLabel(label string)
SetLabel names the entity a registry belongs to, so a firing can be attributed. The framework calls this when it creates the per-entity registry; a hand-built registry can set it too.
type HookType ¶
type HookType int
HookType enumerates the lifecycle hook points for entity operations.
type ListPayload ¶
type ListPayload struct {
Request *http.Request
Where []WhereClause
Results []map[string]any
}
ListPayload is the data argument passed to BeforeList and AfterList hooks.
BeforeList: Request is non-nil, Where starts empty, Results is nil. Hooks call AddWhere() to attach scope filters.
AfterList: Request and Results are non-nil, Where is no longer applied. Hooks may redact, transform or reorder Results, but must preserve the row count. Visibility filtering belongs in BeforeList so counts and pages agree.
REDACTION: masking a field here changes what the caller reads, not what the database filtered and sorted on. The stored value is still a live column, so ?field_like=… and ?sort=field recover it from which rows come back and in what order, while every response shows the mask. Mark such a field NoQuery in its schema.Field so the query surface refuses it. See framework/crud/redaction_oracle_security_test.go.
Masking here covers every HTTP path that returns the row: List, Get, keyset pages, ?include= children (via the child entity's own hooks), _events deliveries, and create/update response bodies. Register the same mask on AfterGet too, each path runs the hook matching the shape it serves, so a to-one ?include= runs the child's AfterGet, the way its own GET /child/{id} route does.
The in-process Go API returns stored values unless the caller passes crud.WithReadHooks, so read-modify-write still works. Every path that runs AfterList refuses a row-count change. On an ?include= payload each row is already keyed to its parent, though sorting Results is harmless there (rows are matched by primary key, so order is free; keep the id when projecting). ?stream=true refuses rather than bypass. A value that must never leave the server raw belongs in a Hidden field, which is enforced in the projection. See the hook-skip matrix in framework/docs/content/hooks-and-transactions.md.
func (*ListPayload) AddWhere ¶
func (p *ListPayload) AddWhere(sql string, args ...any)
AddWhere appends a parameterised WHERE clause. Use $1, $2, … placeholders, one per argument, in order: when the clause is composed into the final query its placeholders are renumbered positionally (by encounter), so pass exactly one argument per placeholder token and do NOT reuse a number as a back-reference to an earlier bind, write the value twice if you need it twice. The clause is parenthesised when composed, so OR/AND inside it cannot leak past framework-injected scopes, and a $N appearing inside a single-quoted string literal is treated as data and left untouched.
type PanicError ¶ added in v0.96.0
type PanicError struct {
// Value is what the hook passed to panic.
Value any
}
PanicError is the error ExecuteHooks returns for a hook that panicked. A panic is a fault in the hook, not a refusal it chose: a caller that answers a Before hook's error with its text (crud answers 400 with it) answers this one as a server error instead, because the panic value is whatever the hook held when it faulted (a DSN, a query, request bytes) and was never written for the client. Error renders the value scrubbed of control bytes, for logs.
func (*PanicError) Error ¶ added in v0.96.0
func (e *PanicError) Error() string
type WhereClause ¶
WhereClause is an editable SQL predicate that BeforeList / BeforeGet hooks can append to scope read queries (e.g. inject WHERE user_id = $1). CRUD applies appended clauses to the data query, and, for List, also to the count query, so totals reflect the filtered result.
SECURITY: SQL is appended VERBATIM to the query. Never concatenate caller-controlled values into SQL, always use placeholders ($1, $2, …) and pass values as Args. The framework's query builder takes care of parameter binding; user code that bypasses this is the source of every SQL-injection bug a hook can introduce.
// SAFE: parameterised binding
p.AddWhere("status = $1", "published")
// UNSAFE: string concatenation
p.AddWhere("status = '" + userInput + "'") // SQL INJECTION