Configuration
Configuration has separate layers: runtime GlobalOptions, shared model starting
defaults, per-model/data/root options, model per-method defaults, request args/
options, and trusted service authorization. Data/root routers do not inherit model
defaults; authorization is never a default or wire option. createAccessRuntime()
returns a bound API whose .runtime owns the options and registries.
Global Options
Configure a runtime with acl.setGlobalOptions(...) or the corresponding isolated
API method. Assignment is shallow. Request initialization and hook/persistence
captures are not restarted by later setters.
requestPermissionFielddefaults to_permissionsglobalPermissions(req)returns the permission set for the current requestloggerreceivesdebug,info,warn, anderrorcalls
import acl from '@web-ts-toolkit/access-router';
declare module '@web-ts-toolkit/access-router' {
interface AccessRouterRequestExtensions {
actor?: { permissions: string[] }; // set by application authentication middleware
}
}
acl.setGlobalOptions({
requestPermissionField: '_permissions',
globalPermissions(req) {
return req.actor?.permissions ?? [];
},
});
Null/undefined resolver results establish empty grants, replacing prior runtime-owned values. Application-supplied unowned maps are preserved; same-runtime middleware reuses its initialized state. Populate targets require runtime registration by default.
Default Model Options
These values supply model defaults. Assignment is shallow; nested rule objects are not recursively merged. See Operation access for exact variant/default lookup and live updates.
listHardLimitdefaults to1000documentPermissionFielddefaults to_permissionsidParamdefaults toididFielddefaults to_idparentPathdefaults to/queryRouteSegmentdefaults to__querymutationRouteSegmentdefaults to__mutationoperationAccessdefaults tofalsemodelPermissionPrefixdefaults to an empty string
Include output cannot equal, descend from, or contain documentPermissionField,
including dotted custom fields and equivalent legacy bracket paths. Nested include
levels use the receiving model's setting; violations are BadRequest before target
persistence. See Services.
Model Router Options
Common model options:
basePathpermissionSchemaaccepts booleans, permission names, or per-operation rule objectsalwaysSelectFieldsdocPermissionsbaseFilteroverrideFilterdecoratedecorateAllvalidatepreparetransformafterPersistonChangebeforeDeleteafterDeleterequestSchemasdefaults
Live property helpers include:
listHardLimitdocumentPermissionFieldidFieldresolveIdFilteroperationAccess
basePath, parentPath, idParam, queryRouteSegment, and
mutationRouteSegment determine route shape and are fixed after construction.
Authority, hook inputs and metadata
Router defaults, including dotted setters, contain query/presentation settings only.
Per-call service authorization, document handles, prepared/execution state and removed
service overrides are not defaults or HTTP/OpenAPI/client options. Supply trusted
server-only policy to the service acquired from router.getService(req) instead.
Missing policy dimensions use request authority and never inherit a prior call's elevation.
alwaysSelectFields declares persisted inputs needed by document-permission hooks beyond
selected output, such as ownerId. Hooks see the untrimmed projected document; fetch-only
inputs do not grant output. The immutable call captures configuration, while each document/
scope/access stage has separate hook grants and decisions. Opaque result-context handles
preserve them after those inputs and metadata trim. See Services.
Internal document grants resolve independently of includePermissions,
includeFieldPermissions, stripPermissionsField, disableFieldPermissions and exposed-key
limits. These switches govern request-user metadata serialization; temporary backend grants
never forge _view/_edit. overrideFilter remains the query-transform hook and runs once
even in unrestricted row mode; scope/unrestricted replace only baseFilter authority.
skim is a deprecated compatibility no-op in canonical model operations. True, false
and omission all retain authorization, document grants, virtuals and decoration.
lean separately chooses Mongoose hydration; it does not skip field enforcement.
At each method resolver, non-nullish supplied values win over its defaults bucket,
then built-ins. Public wrappers resolve public*Args/public*Options before internal
methods; already-resolved flags take precedence over internal defaults. Public upsert
uses its create/update branch's public bucket. A model service captures current presentation
defaults from its originating runtime per call; per-call model authority captures current
policy before awaiting hooks. Retained services keep their original core, actor, model
connection and related registry across a same-request runtime handoff. Base-filter cache
entries are reused only for the same model/access and policy function.
Request base-filter caches remain owned by their request/runtime/access.
Operation Access
Use OperationAccess in ModelRouterOptions.operationAccess for all fourteen
model keys: basicList/advancedList, basicRead/advancedRead,
basicCreate/advancedCreate, basicUpdate/advancedUpdate,
basicUpsert/advancedUpsert, basicCount/advancedCount, and
basicDistinct/advancedDistinct. The complete routing matrix
maps endpoints to these keys. Data routers use four list/read keys; subdocument
fields use four nested list/read keys.
An exact variant replaces the base guard. Only undefined inherits the existing
base exact → operationAccess.default → scalar operationAccess shorthand
resolution. False, an empty array, and a false-returning hook are final. With no
other fallback/default variant configured:
| Rules | Basic list | Advanced list | Root list entry |
|---|---|---|---|
list: true | allowed | allowed | allowed |
list: false | denied | denied | denied |
list: true, basicList: false | denied | allowed | allowed |
list: true, advancedList: false | allowed | denied | allowed |
list: false, basicList: true | allowed | denied | denied |
only basicList: true | allowed | denied | denied |
list: true, basicList: undefined | allowed | allowed | allowed |
import acl, { type ModelRouterOptions } from '@web-ts-toolkit/access-router';
const baseOnly = {
operationAccess: { list: true, read: true },
permissionSchema: { name: true },
} satisfies ModelRouterOptions;
const advancedReadsOnly = {
basePath: '/users',
operationAccess: { list: true, read: true, basicRead: false },
permissionSchema: { name: true },
} satisfies ModelRouterOptions;
const basicListsOnly = {
operationAccess: { list: true, advancedList: false },
} satisfies ModelRouterOptions;
const permissionBased = {
operationAccess: { list: true, read: true, advancedList: 'isAdmin', advancedRead: ['canRead', 'isAdmin'] },
permissionSchema: { name: { list: true, read: true } },
} satisfies ModelRouterOptions;
const shorthand = { operationAccess: 'canUseApi' } satisfies ModelRouterOptions;
const withDefault = {
operationAccess: { default: 'canUseApi', list: true, basicRead: false },
} satisfies ModelRouterOptions;
acl.setDefaultModelOptions({ operationAccess: { list: true, read: true } });
acl.setDefaultModelOption('operationAccess.advancedRead', 'isAdmin');
const defaultRead = acl.getDefaultModelOption('operationAccess.advancedRead');
Among top-level model routes, basicRead: false denies only GET-by-id (and its HEAD fallback), leaving both
advanced read POSTs authorized by read: true. List/new/count/distinct/mutation
guards remain independent. Denial is HTTP 401, with endpoints still present
in getEndpoints() and OpenAPI.
Rule values are Validation (boolean | string | string[] | GuardHook). Strings
AND space-separated permissions; arrays OR their entries. Hooks receive
AccessRouterPermissions, bind this to the request, and may be async. Only the
selected guard runs; operational errors do not become permission fallback.
Configure permission names with globalPermissions. Field grants stay in
PermissionSchema/FieldOperationAccess under base names, not variant names.
Model default specificity
Model exact variant lookup consults the runtime's same exact default path if the
model value is undefined, before base fallback. A default advancedRead can
therefore outrank a model's read, default, or shorthand; an exact model variant
outranks that default variant. A model object still replaces its stored
operationAccess object shallowly, rather than recursively inheriting all siblings.
Copied default values remain stored until updated. Later default setter calls do
not broadcast replacement over existing model values; clearing a stored variant
may expose the current exact runtime default. getDefaultModelOption reads a
path exactly, without variant/base or .default resolution.
getDefaultModelOptions() returns a frozen snapshot. Data routers have no model
default inheritance.
Subdocument field rules
For subs.comments.basicList, precedence is exact field variant → field list
→ defined field scalar/closed object → top-level basicList only for an absent
field → existing top-level list fallback. Read/advanced variants follow the
same ordering.
import type { ModelRouterOptions } from '@web-ts-toolkit/access-router';
const options = {
operationAccess: {
list: true,
read: true,
subs: {
comments: { list: true, read: true, basicList: false, advancedRead: 'isAdmin' },
items: { read: true }, // Closed: omitted list rules deny both items list routes.
attachments: 'isAdmin',
},
},
} satisfies ModelRouterOptions;
A defined object without the applicable operation cannot be reopened by top-level
allows. Exact field lookups retain model-default specificity; an inherited defined
field also stays closed. There is no nested .default fallback. Legacy scalar subs rules remain
accepted, but do not act as an umbrella for absent fields; use explicit field rules.
Route grants retain base field/row/hook, root-entry, related-target, read-miss fallback, upsert branch, and mutation-response policies. See Services.
Request Complexity And Correlated Limits
requestComplexity bounds apply to every read/list/count request, including
correlated-include templates and their per-parent expansions:
maxDepth(default8),maxNodes(default500),maxLogicalClauses(default50),maxInValues(default100),maxBulkItems(default100),maxIncludeCount(default10),maxSubQueryCount(default10),maxBulkConcurrency(default10),maxHookConcurrency(default10)maxCorrelatedQueries(default100): cumulative inner target executions per request/runtime (each executed per-parent read/list/count counts 1; nested levels multiply). Exceeding it fails the request/affected root entry.maxCorrelatedDepth(default5): nesting depth of correlated includes.
Each correlated entry counts toward maxIncludeCount and its markers count
as nodes; template $in lengths count toward maxInValues. After
per-parent substitution the expanded filter is revalidated against
maxNodes/maxDepth/maxInValues/maxLogicalClauses, so a short template
expanding against a large parent array can still be rejected. Prefer tight parent
filters and small per-parent limits to bound relationship work.
Persistence admission
Both concurrency limits are normalized and captured together on first use of a request/runtime's private work state. Existing owners retain that capture after configuration changes; new requests capture the new limits.
maxBulkConcurrency bounds awaited adapter/document persistence operations across
model-service calls, root entries, subqueries, legacy includes and nested correlated
levels sharing one request/runtime. Each request and runtime has an independent pool;
services capture their runtime at construction. Admission awaits lazy queries,
save/delete and service-triggered post-write populate. Bulk create admits one
singleton-array adapter call per item, preserving result order and submitting every
item even if another rejects.
Recursive orchestration holds no permit while awaiting descendants, so limit 1
supports nested includes. Internal map/run bounds remain per invocation.
RootRouter.maxConcurrentOperations separately limits whole root operations per
order group. Permits release on success/error with FIFO transfer; release does not
cancel submitted siblings or refund the cumulative correlated-query budget.
This is an adapter-operation ceiling, not a count of MongoDB driver commands: one operation can issue multiple commands via Mongoose middleware/populate. Router ACL, prepare, afterPersist and decorate run outside the permit; Mongoose middleware is inside the awaited operation. Direct DB/network calls in trusted hooks are outside admission. This is not a process-wide connection limit, transaction/optimistic-edit guard or throughput guarantee. Controlled nested-query tests measured peak collection calls falling from 9 to 3 at limit 3; that does not bound arbitrary middleware fan-out.
Hook admission and nested service calls
maxHookConcurrency (default 10) counts independent hook chains across one
request/runtime's services, root entries, includes, populate and subdocuments. The
pool covers model document-grant hooks (including pre-write/helper/owning-parent
stages), virtual getters, model/data decorate/decorateAll, service output
decorators and data request-field selection/trimming stages. Other guards, row/ID
filters, model field-rule callbacks and write lifecycle hooks are outside this pool.
A hook can await a nested service at limit 1. Private async context lets its
descendant hooks reuse the chain, while each callback scope admits one immediate
child branch at a time, FIFO through descendant settlement. Promise.all sibling
hook stages therefore serialize rather than bypassing admission. Waiting ancestors
are additional frames, not additional permits: the limit bounds independent leaf
branches, not all unsettled callbacks. Reentrant depth is capped at 64 admission
scopes per chain, including internal wrappers; overflow uses the existing hook
failure policy. Settled scopes close to new reentry and drain queued children even
on failure; detached late continuations reacquire root admission. Await hook work
and avoid cross-branch dependency cycles. Root and child queues each transfer FIFO.
Row/traversal maps retain per-call worker limits and stable output/context order. Hook and persistence pools are separate; no persistence permit is held over hooks or orchestration. Trusted direct DB/network fan-out, total queued work and query counts are outside these limits. They do not provide process-wide connection limits or automatic N+1 batching.
OpenAPI Router Options
createOpenApiRouter(...) accepts:
titleversiondescriptionserversjsonPathdefaulting to/openapi.jsondocsPathdefaulting to/docs, orfalseto disable Swagger UIswaggerUiCssUrlswaggerUiBundleUrl
Data Router Options
Common data options:
databasePathoperationAccesssupportslist/read,default, scalar shorthand, andbasicList/advancedList/basicRead/advancedReadpermissionSchemaaccepts booleans, permission names, or per-operation rule objectsbaseFilteroverrideFilterdecoratedecorateAllrequestSchemas
Router-specific options:
listHardLimitidParamidFieldresolveIdFilterparentPathqueryRouteSegmentoperationAccessdataName
Set both idParam (Express parameter name) and idField (lookup field) explicitly
for data identifier routes; data has no model-default layer. Callable resolveIdFilter
takes precedence over idField for both model and data lookups. parentPath describes
an external mount prefix in metadata/logging; mount that prefix in Express yourself.
Data owns configured nested/native values and rejects unsupported values at assignment.
Services capture one dataset version and detach only selected/page-returned values.
Frozen snapshot native properties return detached mutable copies on access; native
identity is not stable. Replace through router.data(next) or runtime data setters.
See the installed README's “In-memory data ownership” for the exact supported values.
Option APIs
The runtime exposes getter/setter pairs for each option group:
acl.setGlobalOptions(...),acl.setGlobalOption(...),acl.getGlobalOptions(),acl.getGlobalOption(...)acl.setModelOptions(...),acl.setModelOption(...),acl.getModelOptions(),acl.getModelOption(...)acl.setDefaultModelOptions(...),acl.setDefaultModelOption(...),acl.getDefaultModelOptions(),acl.getDefaultModelOption(...)
Use router instance methods when you want to mutate one router in place.
import mongoose from 'mongoose';
import { createAccessRuntime } from '@web-ts-toolkit/access-router';
const runtime = createAccessRuntime();
const User = mongoose.model('ConfigurationVariantUser', new mongoose.Schema({ name: String }));
const router = runtime.createRouter(User, {
basePath: '/users',
operationAccess: { list: true, read: true },
permissionSchema: { name: true },
});
router.operationAccess('basicRead', false); // Preserve the other keys.
router.operationAccess('subs.comments.basicRead', false);
router.set('operationAccess.advancedList', 'isAdmin');
router.setOption('operationAccess.basicRead', undefined); // Restore inheritance.
runtime.setModelOption(router.modelName, 'operationAccess.basicRead', false);
router.operationAccess({ list: true, read: true, basicRead: false }); // Whole rule replacement.
router.setOptions({ operationAccess: { list: true, advancedList: false } }); // Same shallow assignment.
const current = runtime.getModelOptions(router.modelName);
const exactVariant = runtime.runtime.getExactModelOption(router.modelName, 'operationAccess.basicRead');
const dataRouter = runtime.createDataRouter('VariantData', {
idParam: 'id', data: [], operationAccess: { list: true, read: true },
});
dataRouter.setOption('operationAccess.basicRead', false);
dataRouter.runtime.setDataOption('VariantData', 'operationAccess.advancedRead', true);
const currentData = dataRouter.runtime.getDataOptions('VariantData');
set({ operationAccess: ... }), setOption('operationAccess', ...), and property
helper object forms replace that entire object shallowly. Dotted paths are setter
arguments, not flat dotted properties in constructor options. A path update under
scalar shorthand starts an object; use default to retain the broad base fallback.
Use typed set/setOption with undefined to clear; property-helper key updates
are for defined values.
router.options is a frozen construction-time snapshot. Getter results are frozen
snapshots too (with data's native-copy behavior above); fetch again for current stored values. Generic
getModelOption/getDataOption use nested-option fallback, not route-specific
variant-to-base resolution. Exact getters expose the variant path (with exact model
default fallback); request guards resolve live runtime options.