Skip to main content

Routing

access-router gives you three router classes:

  • RootRouter for batch requests
  • ModelRouter for Mongoose models
  • DataRouter for in-memory collections

The main factory is acl.createRouter(...).

Router Factories​

import mongoose from 'mongoose';
import acl from '@web-ts-toolkit/access-router';

const modelRouter = acl.createRouter('User', { basePath: '/users' });
const dataRouter = acl.createDataRouter('fruit', { basePath: '/fruit', data: [] });
const rootRouter = acl.createRouter({ basePath: '/batch', operationAccess: true });

const typedRouter = acl.createRouter(
mongoose.model('User', new mongoose.Schema({ name: String })),
{ basePath: '/users' },
);

createAccessRuntime() returns a fresh acl instance with isolated runtime state.

Mounting​

Each router exposes:

  • router for the internal JsonRouter instance
  • routes for the underlying Express router
  • fullBasePath for the normalized mounted path

Use combineRoutes(...) to mount several routers at once.

import express from 'express';
import acl, { combineRoutes } from '@web-ts-toolkit/access-router';

app.use(combineRoutes(fruitRouter, userRouter, rootRouter));
app.use(acl.combineRoutes(fruitRouter, userRouter, rootRouter));

combineRoutes(...) accepts ModelRouter, DataRouter, RootRouter, or a plain Express Router.

Model Router Routes​

Default route segments are __query and __mutation.

Base guardBasic endpoint → overrideAdvanced endpoint → override
listGET /base → basicListPOST /base/__query → advancedList
readGET /base/:id → basicReadPOST /base/__query/:id, POST /base/__query/__filter → advancedRead
createPOST /base → basicCreatePOST /base/__mutation → advancedCreate
updatePATCH /base/:id → basicUpdatePATCH /base/__mutation/:id → advancedUpdate
upsertPUT /base → basicUpsertPUT /base/__mutation → advancedUpsert
countGET /base/count → basicCountPOST /base/count → advancedCount
distinctGET /base/distinct/:field → basicDistinctPOST /base/distinct/:field → advancedDistinct

GET /base/new and DELETE /base/:id keep unpaired new and delete guards. Filtered count/distinct POSTs are advanced even without an advanced path segment. Custom idParam, queryRouteSegment, and mutationRouteSegment change the paths, not the guard names.

An exact variant replaces its base guard; only undefined inherits. Denials return HTTP 401 Unauthorized before validation/service dispatch. All endpoints remain registered in router.router.getEndpoints() and OpenAPI. HEAD fallback uses its corresponding GET's basic guard. See Configuration for fallback, model-default, and closed-subdocument-field behavior.

import acl from '@web-ts-toolkit/access-router';

// User is already registered with the default runtime.
const router = acl.createRouter('User', {
basePath: '/users',
operationAccess: { list: true, read: true, basicRead: false },
permissionSchema: { name: true },
});
router.operationAccess('basicRead', false); // Live key update.

Among top-level model routes, this denies GET/HEAD /users/:id only. Both advanced reads and GET /users remain allowed, subject to ordinary field/row policy; new/count/distinct/mutation guards are unaffected. Subdocument reads use the documented field fallback. With { list: true, advancedList: false }, only POST /users/__query is denied among the list pair.

Basic model list GETs accept select and sort query params, for example GET /base?select=name&sort=name%20-createdAt. sort is a single space-separated list of fields, prefixed with - for descending; order sets multi-field priority. Sort authorization and sortableFields/ stripDisallowedSort apply exactly as on advanced lists. Omitted sort keeps backend defaults; ?sort= disables them. Basic GET-by-id reads also accept select.

Sub-document routes are generated for every referenced subpath discovered on the model schema.

For each subdocument path sub, these routes are added:

Base guardBasic endpoint → overrideAdvanced endpoint → override
subs.<sub>.listGET /base/:id/<sub> → subs.<sub>.basicListPOST /base/:id/<sub>/__query → subs.<sub>.advancedList
subs.<sub>.readGET /base/:id/<sub>/:subId → subs.<sub>.basicReadPOST /base/:id/<sub>/:subId/__query → subs.<sub>.advancedRead

Unpaired subdocument mutations retain their base guards:

  • PATCH /base/:id/<sub> and PATCH /base/:id/<sub>/:subId → subs.<sub>.update
  • POST /base/:id/<sub> → subs.<sub>.create
  • DELETE /base/:id/<sub>/:subId → subs.<sub>.delete

That gives you generated list, advanced list, bulk update, read, advanced read, update, create, and delete operations for each discovered subdocument collection.

Data Router Routes​

Default route segment is __query.

Base guardBasic endpoint → overrideAdvanced endpoint → override
listGET /base → basicListPOST /base/__query → advancedList
readGET /base/:id → basicReadPOST /base/__query/:id, POST /base/__query/__filter → advancedRead

These four keys use data-router options only, with no model-default inheritance. Set idParam: 'id' explicitly for the illustrated data identifier paths. Both advanced reads share advancedRead; HEAD follows the basic GET guard.

Root Router​

RootRouter handles batch payloads that dispatch to model or data operations.

Use it when you want a single request to run multiple operations in order.

Construct it with the single-options overload api.createRouter({ basePath: '/batch', operationAccess: true }) and mount .routes. Its runtime must also own the target registrations. Limits default to maxBatchEntries: 100, maxOrderGroups: 25, maxConcurrentOperations: 10. maxOrderGroups bounds both distinct group count and each numeric order: at the default, orders are integers in 0..25 inclusive (omission is 0), but at most 25 different values can occur. A lone order 26 or all 26 values 0..25 reject before entry dispatch. Invalid/non-positive/unsafe constructor limits use the defaults.

Groups execute in ascending order; operations within each group share the whole- operation concurrency ceiling. Results retain input order. Hook-chain and persistence pools are separate request/runtime budgets. Batches are non-transactional; unexpected rejection can leave already-committed writes.

Each batch item uses:

  • target: model or data
  • name: the model or data router name
  • op: the operation name
  • order: optional execution bucket

Operation-specific fields are kept at the top level:

  • id for single-document operations
  • sub and subId for sub-document operations
  • field for distinct
  • filter for query-based lookups
  • data for writes
  • args for selection, populate, paging, and task inputs
  • options for behavior switches such as permissions and counts

Direct routes and root entries are request-only. Trusted service authorization, opaque document handles, private prepared/execution controls and the removed overrides container are rejected in control slots, including nested include/populate/$$sq options and validator-transformed envelopes. They are not client request features. Business data fields with those names remain valid under ordinary permissions. Use internal services from server code for per-call authority; generated route guards still apply independently.

Root entries retain existing base operation names and target base entry checks; basicList/advancedRead (and the other variants) are not valid op values. Allowing a direct variant does not override a denied root base entry. A denied entry has status 401 inside the HTTP 200 batch envelope. The enclosing root endpoint's own operationAccess remains a scalar guard.

Root entries accept the same include shapes as direct routes: legacy localField/foreignField joins and correlated entries (mode: 'correlated' with { "$parent": "<field>" } markers resolved per parent against the immediate parent snapshot). Direct and root validators share one schema, so verdicts agree; malformed correlated input is a controlled BadRequest.

The response wraps each operation as:

{
index: number;
target: 'model' | 'data';
name: string;
op: string;
statusCode: number;
message: string;
result: unknown;
}

result keeps the public service response instead of flattening it into a second batch-only format. Internal context(s), prepared state, document handles and query diagnostics are omitted.

OpenAPI Router​

access-router can also mount a separate documentation router:

app.use(acl.createOpenApiRouter());

By default this exposes:

  • GET /openapi.json
  • GET /docs

You can customize those paths with jsonPath and docsPath, or disable Swagger UI entirely with docsPath: false.

Mutability​

Build-time route-shape options are immutable after router construction.

Model router build-time options:

  • basePath
  • parentPath
  • idParam
  • queryRouteSegment
  • mutationRouteSegment

Data router build-time options:

  • basePath
  • parentPath
  • idParam
  • queryRouteSegment

Behavior options such as filters, hooks, permissions, and defaults can be changed later with set(...), setOption(...), or setOptions(...).

Operation variants are live behavior options. router.operationAccess('basicRead', false) updates one key. Passing an object replaces the whole operationAccess object shallowly, not a deep merge. router.options stays a frozen construction snapshot; read current stored values through the owning runtime's getters.