# @web-ts-toolkit/access-router ACL-aware Express routers and in-memory data services for Mongoose-backed APIs. ## Main Patterns ```ts import express from 'express'; import mongoose from 'mongoose'; import acl, { permissionsPlugin } from '@web-ts-toolkit/access-router'; acl.setGlobalOptions({ requestPermissionField: '_permissions', globalPermissions(req) { return req.headers.user === 'admin' ? ['isAdmin'] : []; }, }); const userSchema = new mongoose.Schema({ name: String, role: String, public: Boolean }); userSchema.plugin(permissionsPlugin, { modelName: 'User' }); mongoose.model('User', userSchema); const userRouter = acl.createRouter('User', { basePath: '/users' }); // Also: pass a mongoose.Model instance to preserve a non-default connection. // acl.createRouter(UserModel, { basePath: '/users' }); const fruitRouter = acl.createDataRouter('fruit', { basePath: '/fruit', idField: 'id', data: [{ id: 'apple', name: 'Apple' }], }); const docsRouter = acl.createOpenApiRouter({ title: 'Example API', version: '1.0.0' }); ``` Validation adapters: ```ts import { z } from 'zod'; import { fromZod, defineRequestSchema } from '@web-ts-toolkit/access-router'; const validator = fromZod(z.object({ name: z.string() })); const schema = defineRequestSchema(validator); ``` Isolated runtime (no global state shared with default `acl`): ```ts import { createAccessRuntime } from '@web-ts-toolkit/access-router'; import mongoose, { type Model } from 'mongoose'; type User = { name?: string }; const runtime = createAccessRuntime(); runtime.setGlobalOptions({ globalPermissions: () => [] }); const userSchema = new mongoose.Schema({ name: String }); const UserModel: Model = mongoose.model('User', userSchema); runtime.createRouter(UserModel, { basePath: '/users' }); ``` Subpath imports: ```ts import { copyAndDepopulate, type CopyAndDepopulateOutput } from '@web-ts-toolkit/access-router/processors'; import { parseBody, Codes, MIDDLEWARE } from '@web-ts-toolkit/access-router/advanced'; type DepopulatedItems = { items: string[]; snapshot: Array<{ _id: string; name: string }> }; const typed = copyAndDepopulate( { items: [{ _id: 'x', name: 'x' }] }, [{ src: 'items', dest: 'snapshot' }], { mutable: false }, ); const conservative: CopyAndDepopulateOutput = copyAndDepopulate({ items: [{ _id: 'x' }] }, [ { src: 'items', dest: 'snapshot' }, ]); void [typed, conservative]; ``` Correlated includes: besides legacy `localField`/`foreignField` joins, `include` entries accept `mode: 'correlated'` with `{ "$parent": "" }` markers in filter value positions (or as the whole `id`). Each entry runs per parent against the immediate parent snapshot: reads attach doc-or-`null`, lists attach per-parent paginated arrays, counts attach numbers (missing/null refs attach `null`/`[]`/`0` with no query). Plain `'$special'` strings stay literal; match literal `{ "$parent": "x" }` objects with `{ "$escape": { "$parent": "x" } }`. Identifier behavior, per-op target authorization (denials fail the whole request with zero target queries), and bounds (`maxIncludeCount`, revalidated `maxNodes`/`maxDepth`/`maxInValues`/ `maxLogicalClauses`, `maxCorrelatedQueries` default 100, `maxCorrelatedDepth` default 5) follow the README "Correlated Includes" section. Wire types (`ParentRef`, `CorrelatedInclude`, `Include`) are available from `@web-ts-toolkit/access-router/advanced` without deep imports. ## Gotchas - peer dependencies: `express >= 5` and `mongoose >= 8` - default export `acl` is preferred for the default-runtime API; named exports are also available - `acl.createOpenApiRouter(options)` uses the default runtime; the standalone `createOpenApiRouter(runtime, options)` export requires the runtime as the first argument - `createRouter('User', ...)` accepts a Mongoose model name OR a `mongoose.Model` instance — passing the instance registers the model with the runtime and preserves non-default `mongoose.createConnection()` instances - isolated runtimes do not silently read process-global `mongoose.models`; register the model first or pass the `mongoose.Model` instance to `runtime.createRouter(...)` - `/advanced` does NOT export `acl`, `defaultRuntime`, `createRouter`, or `createAccessRuntime` — those live on the root entry only - `/processors` `copyAndDepopulate(...)` defaults to conservative `CopyAndDepopulateOutput`; pass an explicit output type when the transformed shape is known. Unsafe paths and missing id fields throw plain `Error` instances. - `createDataRouter(...)` holds data in-memory; use it for lightweight/non-persisted resources - ACL filter value `false` is a terminal denial. Trusted `overrideFilter` hooks may replace ordinary filters before base-filter composition, but are not called for an existing denial; returning `false` also denies the query. - Shared-request cross-runtime composition keeps independent state: runtime B never reuses runtime A's permissions or base-filter cache, even with the same `requestPermissionField` name. Same-runtime middleware repeats reuse state without rerunning `globalPermissions`. Pre-populated request permission fields are application-supplied and preserved. - Advanced mutation routes persist the final parsed body: nested `data` validators run before the whole-body (`default`) validator, and only final `data`/`select`/`populate`/`tasks` plus allowed `options` (`includePermissions`/`populateAccess`, plus `returningAll` for update/upsert) reach the service. Missing body options fall back to `returning_all`/`include_permissions` query params. - `package.json` declares `sideEffects` array listing the bundled runtime entries that run the lazy `mongoose-schema-jsonschema` patch on first runtime construction — bundlers must retain those entries ## Pointers - README: installation, quickstart (Express + Mongoose end-to-end), main exports, runtime isolation, createRouter overloads - website/docs/packages/access-router/: full documentation (routing, configuration, hooks, validation, openapi)