hook

package
v0.56.0 Latest Latest
Warning

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

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

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

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

func (hr *HookRegistry) RegisterHook(hookType HookType, fn HookFunc)

RegisterHook appends a hook function for the given hook type. Hooks execute in registration order.

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
)

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