Skip to main content

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',
});