Skip to main content

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.

  • requestPermissionField defaults to _permissions
  • globalPermissions(req) returns the permission set for the current request
  • logger receives debug, info, warn, and error calls
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.

  • listHardLimit defaults to 1000
  • documentPermissionField defaults to _permissions
  • idParam defaults to id
  • idField defaults to _id
  • parentPath defaults to /
  • queryRouteSegment defaults to __query
  • mutationRouteSegment defaults to __mutation
  • operationAccess defaults to false
  • modelPermissionPrefix defaults 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:

  • basePath
  • permissionSchema accepts booleans, permission names, or per-operation rule objects
  • alwaysSelectFields
  • docPermissions
  • baseFilter
  • overrideFilter
  • decorate
  • decorateAll
  • validate
  • prepare
  • transform
  • afterPersist
  • onChange
  • beforeDelete
  • afterDelete
  • requestSchemas
  • defaults

Live property helpers include:

  • listHardLimit
  • documentPermissionField
  • idField
  • resolveIdFilter
  • operationAccess

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:

RulesBasic listAdvanced listRoot list entry
list: trueallowedallowedallowed
list: falsedenieddenieddenied
list: true, basicList: falsedeniedallowedallowed
list: true, advancedList: falsealloweddeniedallowed
list: false, basicList: truealloweddenieddenied
only basicList: truealloweddenieddenied
list: true, basicList: undefinedallowedallowedallowed
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 (default 8), maxNodes (default 500), maxLogicalClauses (default 50), maxInValues (default 100), maxBulkItems (default 100), maxIncludeCount (default 10), maxSubQueryCount (default 10), maxBulkConcurrency (default 10), maxHookConcurrency (default 10)
  • maxCorrelatedQueries (default 100): 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 (default 5): 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:

  • title
  • version
  • description
  • servers
  • jsonPath defaulting to /openapi.json
  • docsPath defaulting to /docs, or false to disable Swagger UI
  • swaggerUiCssUrl
  • swaggerUiBundleUrl

Data Router Options​

Common data options:

  • data
  • basePath
  • operationAccess supports list/read, default, scalar shorthand, and basicList/advancedList/basicRead/advancedRead
  • permissionSchema accepts booleans, permission names, or per-operation rule objects
  • baseFilter
  • overrideFilter
  • decorate
  • decorateAll
  • requestSchemas

Router-specific options:

  • listHardLimit
  • idParam
  • idField
  • resolveIdFilter
  • parentPath
  • queryRouteSegment
  • operationAccess
  • dataName

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.