hook

package
v0.63.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Index

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).

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

type HookFunc func(ctx context.Context, data any) error

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). Return an error to cancel the operation (for Before* hooks) or log the failure (for After* hooks).

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

func (hr *HookRegistry) ExecuteHooks(ctx context.Context, hookType HookType, data any) error

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.

const (
	BeforeCreate HookType = iota
	AfterCreate
	BeforeUpdate
	AfterUpdate
	BeforeDelete
	AfterDelete
	BeforeList
	AfterList
	BeforeGet
	AfterGet
)

func (HookType) String added in v0.59.0

func (h HookType) String() string

String names a hook type as it appears in coverage manifests and diagnostics — lower-case, matching the `OnBeforeCreate` API spelling.

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 mutate Results in place (redact fields, drop rows, etc.).

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. On an ?include= payload a hook may not change the row count — 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 WhereClause

type WhereClause struct {
	SQL  string
	Args []any
}

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL