Hooks
Model and data routers support request-time hooks for ACL decisions, filtering, decoration, and persistence side effects.
Lifecycle
Model router flow:
- access and query shaping:
operationAccess,overrideFilter,baseFilter - pre-write:
validate,prepare,transform - post-persist:
afterPersist - update diff side effects:
onChange - delete flow:
beforeDelete,afterDelete - response shaping:
docPermissions,decorate,decorateAll
Data router flow is simpler and mainly uses operationAccess, baseFilter, overrideFilter, decorate, and decorateAll.
Common Hook Signatures
All of these hooks run with this bound to the current Express request.
baseFilter
Return a filter, or true / null / undefined to leave the query unmodified.
Return false for terminal query denial. Model-service exists returns Forbidden,
not a successful miss, and public read tryList cannot revive that denial.
Integrated internal service row policies use this request scope by default. A trusted
scope replaces it; unrestricted skips it. Neither changes write-field permissions.
overrideFilter
Rewrite the ordinary filter once before the chosen row scope is applied. This configured
hook/setter remains supported after service overrides removal and runs in request, scope
and unrestricted row modes. Its user-dependent restrictions/false denial remain effective;
mandatory relationship constraints and trusted scope are ANDed afterward. An early false
query/identifier/scope skips work as applicable; a later hook denial cannot undo subqueries
already needed to parse a read/list filter. Move replaceable user row restrictions into
baseFilter when migrating old object overrides.filter; there is no transform-bypass option.
See service denial contracts for migration,
existence results and HTTP status mapping.
validate
Return true to allow the write, false to reject it, or an error array to surface validation errors.
prepare
Trusted application hook after validation of selected client data. Output is not
re-filtered and may add protected/server-only fields, including dotted Mongoose paths.
For updates, plain-object/array output at strict ancestors of admitted policy paths
applies supplied children while preserving omitted siblings. Other paths remain whole
assignments. Returning an empty partial container or omitting a parent does not delete
it; explicit null/undefined can clear/unset it. An entirely null/undefined result
supplies no assignments. Do not blindly copy unfiltered context.originalData back in.
transform
Mutate the live Mongoose document after authorized update assignment and before save.
For intentional whole-parent replacement use doc.set('profile', replacement).
Migration: prepare hooks that relied on omitted children deleting a partial parent
must now make that replacement explicit here (or grant the client the whole field).
The update order is validation → prepare → assigned-document transform → save →
afterPersist → changes; snapshots/diffs reflect persistence. Subdocument writes retain
their own lifecycle and do not gain these model hooks.
Router lifecycle hooks run outside persistence admission. Mongoose middleware runs
inside its awaited adapter/document operation, which may issue multiple driver
commands; direct DB/network calls from application hooks are not governed by
maxBulkConcurrency. See Configuration.
afterPersist
Run after persistence and before response shaping.
beforeDelete / afterDelete
Use these for audit logs, cache invalidation, and external side effects.
docPermissions
Return full request-user document grants from the untrimmed projected input. Declare
additional persisted inputs with alwaysSelectFields (for example ownerId); the hook does
not implicitly fetch all stored fields. Integrated output resolves it once per document/
scope/access stage independently of skim and permission metadata switches. Pre-write and
post-write output are distinct stages.
Serialization on the configured permission field is separate: disabling/stripping/limiting metadata does not erase internal grants. Backend output grants do not forge request-user metadata. Later named output helpers reuse original decisions via the opaque result-context handle, even after ownerId and metadata trim; a fresh raw policy is new evaluation, not document-state reuse. See Services.
decorate / decorateAll
Use these trusted application stages (and tasks) after authorization/finalization. They can intentionally remove/construct output; no automatic second authorization pass is promised. Arbitrary replacements/mutations do not acquire valid same-stage document handles.
Request Augmentation
Augment the package types when you want custom request fields or permission names:
import '@web-ts-toolkit/access-router';
declare module '@web-ts-toolkit/access-router' {
interface AccessRouterPermissionMap {
isAdmin?: boolean;
}
interface AccessRouterRequestExtensions {
requestId?: string;
}
}
Guard Helper
guard(...) is a route middleware helper.
It accepts:
- a permission string
- a permission string array
- a hook function
- a model condition object
router.router.get('/admin', guard('isAdmin'), async () => {
return { ok: true };
});
Permissions Plugin
permissionsPlugin adds only a hydrated Mongoose getter alias (default permissions)
for existing _doc[documentPermissionField] metadata, usually _permissions.
It uses active-runtime options, falling back to the default runtime outside managed
execution. It does not compute grants, authenticate an actor or enable field
enforcement; routers/services enforce their policies without the plugin, including
lean results. The alias is separate from package-level computed virtuals.
schema.plugin(permissionsPlugin, {
modelName: 'User',
virtualPermissionField: 'permissions',
});