@web-ts-toolkit/access-router 0.43.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -104,6 +104,17 @@ snapshots. Mutating the original options object, a fetched options snapshot, or
104
104
  not change live runtime policy or served in-memory records. Replace configured data through `router.data(next)`
105
105
  or `setDataOption(name, 'data', next)`.
106
106
 
107
+ ACL filter hooks may return `false` to deny a query. That denial is terminal through identifier, override, and
108
+ base-filter composition. `overrideFilter` is a trusted hook that may replace an ordinary client or identifier
109
+ filter before the base filter is applied, but it is not called for an existing `false` filter and cannot revive
110
+ that denial. Returning `false` from `overrideFilter` also denies the query. Empty or absent filters retain their
111
+ normal unrestricted meaning unless a base filter restricts them.
112
+
113
+ Advanced mutation validators drive dispatch: nested `data` validators run first, then the whole-body (`default`)
114
+ validator sees that transformed envelope, and the service persists only the final parsed `data`, `select`,
115
+ `populate`, `tasks`, and allowed `options`. Missing body options fall back to `returning_all`/`include_permissions`
116
+ query params; only `includePermissions`/`populateAccess` (plus `returningAll` for update/upsert) are forwarded.
117
+
107
118
  ## Main Exports
108
119
 
109
120
  Root entrypoint (`@web-ts-toolkit/access-router`):
@@ -115,12 +126,14 @@ Root entrypoint (`@web-ts-toolkit/access-router`):
115
126
  - `guard(...)` and `GuardModelCondition`, `GuardModelConditionID` types
116
127
  - `combineRoutes(...)` and `createOpenApiRouter(runtime, options)` (note: prefer `acl.createOpenApiRouter(options)` for the default runtime)
117
128
  - validation adapters: `defineRequestSchema(...)`, `fromZod(...)`, `fromYup(...)`, `fromJoi(...)`, `fromAjv(...)`, `fromStandardSchema(...)`, `fromValibot(...)`, `fromArkType(...)`, `fromIoTs(...)`, `fromSuperstruct(...)`, `fromVine(...)`
129
+ - `permissionsPlugin` (src/plugins.ts) — Mongoose schema plugin used as `schema.plugin(permissionsPlugin, { modelName })`
130
+ - logger helpers: `redactFilter`, `redactPayload`, `safeStringify`, `isLevelEnabled` + `OpLogContext` type
118
131
  - option helpers: `setGlobalOptions`, `setGlobalOption`, `getGlobalOptions`, `getGlobalOption`, `setModelOptions`, `setModelOption`, `getModelOptions`, `getModelOption`, `getModelNames`, `getModelJsonSchema`, `setDefaultModelOptions`, `setDefaultModelOption`, `getDefaultModelOptions`, `getDefaultModelOption`
119
132
 
120
133
  Subpath entrypoints:
121
134
 
122
- - `@web-ts-toolkit/access-router/advanced` — low-level runtime context, symbols (`MIDDLEWARE`, `PERMISSIONS`, ...), enums (`Codes`, `StatusCodes`), internals (`parseBody`, `parseQuery`, request schemas). Does NOT export `acl`, `defaultRuntime`, or router-creation helpers.
123
- - `@web-ts-toolkit/access-router/processors` — `copyAndDepopulate` and its `ProcessCopy` / `CopyAndDepopulateOptions` / `CopyAndDepopulateOutput` types for transforming populated documents.
135
+ - `@web-ts-toolkit/access-router/advanced` — low-level runtime context, symbols (`MIDDLEWARE`, `DATA_MIDDLEWARE`, `PERMISSIONS`, ...), enums (`Codes`, `StatusCodes`), internals (`parseBody`, `parseQuery`, `parseBodyWithSchema`, request schemas). Does NOT export `acl`, `defaultRuntime`, or router-creation helpers.
136
+ - `@web-ts-toolkit/access-router/processors` — `copyAndDepopulate`, `copyPaths`, `movePaths`, `sliceArrays`, `countPaths`, `maskPaths` and processor option types (`ProcessCopy`, `CopyAndDepopulateOptions`, `CopyAndDepopulateOutput`, `ProcessorOptions`, `SliceProcessorOptions`, `ProcessSlice`, `ProcessCount`, `ProcessMask`, `ProcessorOutput`) for transforming populated documents.
124
137
 
125
138
  ## Default runtime vs. isolated runtime
126
139
 
@@ -152,6 +165,15 @@ to `runtime.createRouter(model, options)` or call `runtime.registerModelInstance
152
165
  string-name router. The default `acl` runtime retains string-name compatibility with `mongoose.model(name, schema)` and
153
166
  adopts that exact global model instance into its registry on first lookup.
154
167
 
168
+ Cross-runtime composition on a shared request keeps independent runtime-owned state. When one request passes through
169
+ middleware from runtime A and then runtime B, B initializes its own core, resolves its own `globalPermissions`, and
170
+ uses its own base-filter cache; it never reuses A's permissions or cached filters, even when both runtimes use the
171
+ same `requestPermissionField` name. Repeating the same runtime's middleware on one request reuses that runtime's state
172
+ without rerunning its resolver. Values already present in `requestPermissionField` before any runtime middleware runs
173
+ are treated as application-supplied and preserved. Migration: if you relied on B silently inheriting A's resolved
174
+ permissions on a shared request, set those permissions explicitly before the chain or configure B's `globalPermissions`
175
+ to resolve them.
176
+
155
177
  ## createRouter overloads
156
178
 
157
179
  `createRouter(modelName, options)` — on the default `acl` runtime, accept the Mongoose model name registered with `mongoose.model(name, schema)`. On isolated runtimes, the name must already be registered with that runtime.
@@ -180,6 +202,79 @@ Advanced read/list requests can attach related model data with `include` entries
180
202
  `op: 'count'` returns exact authorized counts and ignores include pagination fields such as `args.limit`.
181
203
  `op: 'list'` materializes authorized related rows through the target model's normal list path, so target `limit`, `page`, `pageSize`, and `listHardLimit` bounds still apply to included rows.
182
204
 
205
+ ## Correlated Includes
206
+
207
+ Besides the legacy `localField`/`foreignField` joins, `include` entries accept
208
+ a correlated variant (`mode: 'correlated'`) that runs a target query **per
209
+ parent document** using values from that parent. References are explicit
210
+ structural markers — `{ "$parent": "<field>" }`, dotted paths allowed —
211
+ recognized only in filter _value_ positions (bare values, field operators
212
+ such as `$eq`/`$in`, `$in` array elements, `$and`/`$or`/`$nor` clauses,
213
+ `$elemMatch` subtrees) and as the whole `id` of identifier reads. Plain
214
+ strings are never references: `'$special'` keeps its literal query meaning.
215
+ Match a literal `{ "$parent": "x" }` object with
216
+ `{ "$escape": { "$parent": "x" } }` (use the `$eq`-wrapped form when the
217
+ literal sits in a bare field position).
218
+
219
+ Wire shapes (the originating method fixes `op`; there is no override):
220
+
221
+ ```json
222
+ { "mode": "correlated", "model": "Org", "op": "read", "path": "org", "id": { "$parent": "orgId" } }
223
+ { "mode": "correlated", "model": "Org", "op": "read", "path": "org",
224
+ "filter": { "_id": { "$parent": "orgId" }, "active": true }, "args": { "select": ["name"] } }
225
+ { "mode": "correlated", "model": "Post", "op": "list", "path": "posts",
226
+ "filter": { "authorId": { "$parent": "_id" }, "title": "$special" },
227
+ "args": { "select": ["title"], "sort": { "createdAt": -1 }, "limit": 5 } }
228
+ { "mode": "correlated", "model": "Post", "op": "count", "path": "postCount",
229
+ "filter": { "authorId": { "$parent": "_id" } } }
230
+ ```
231
+
232
+ Semantics:
233
+
234
+ - References resolve against the **immediate parent** document as fetched
235
+ from persistence (including internally loaded reference fields), before
236
+ include attachment, `decorate` hooks, and task mutation. Nested
237
+ `args.include` entries bind to the target doc of the enclosing include.
238
+ Sibling include output never feeds references.
239
+ - A missing (`undefined`) or `null` reference short-circuits that parent's
240
+ execution to the no-match shape with no target query: `read` attaches
241
+ `null`, `list` attaches `[]`, `count` attaches `0`. Target misses produce
242
+ the same shapes. The output path is always set.
243
+ - Identifier reads use the target's configured identifier behavior
244
+ (`resolveIdentifierFilter`/`genIDFilter`, custom `resolveIdFilter`
245
+ honored) with no read-to-list fallback. Counts use explicit count access
246
+ with count semantics, independent of any list limit. List pagination
247
+ (`skip`/`limit`/`page`/`pageSize`) applies **per parent** — contrast with
248
+ legacy batched list includes, whose pagination applies across the parent
249
+ set. Substituted parent values are data: objects match literally (never
250
+ become operators) and arrays are never flattened.
251
+ - Each correlated entry counts toward `maxIncludeCount`; expanded filters
252
+ are revalidated against `maxNodes`/`maxDepth`/`maxInValues`/
253
+ `maxLogicalClauses`. Total inner executions per request are bounded by
254
+ `maxCorrelatedQueries` (default `100`) and nesting by
255
+ `maxCorrelatedDepth` (default `5`); exceeding any bound fails the whole
256
+ request. Fan-out shares the request-scoped scheduler (no per-parent
257
+ reset). Target authorization denials fail the whole request with zero
258
+ target persistence queries; target runtime errors fail the whole request.
259
+ `path` must be a non-empty valid field path (not `_id`, not `$`-prefixed);
260
+ duplicates in one include array are `BadRequest`; collisions with parent
261
+ fields overwrite (legacy `setDocValue` parity).
262
+ - Referenced parent fields are added to the parent DB select like legacy
263
+ `localField`s, then trimmed unless allowed by field policy and selected.
264
+ A policy-forbidden reference still resolves — only its query effects are
265
+ visible, never the value.
266
+ - Direct count parents carry no `include`; correlated _count_ includes
267
+ attach to read/list parents only. Correlated entries are accepted in both
268
+ direct (`model-router.ts`) and root (`root-router.ts`) validators with
269
+ identical verdicts; malformed correlated input is a controlled
270
+ `BadRequest`, never a silent drop. Legacy entries are unchanged, and
271
+ legacy-shaped entries carrying `$parent` markers are rejected.
272
+
273
+ The TypeScript wire types (`ParentRef`, `CorrelatedInclude`, `Include`) are
274
+ reachable without deep imports via `@web-ts-toolkit/access-router/advanced`
275
+ (which re-exports `./interfaces`). See the website `services`,
276
+ `configuration`, `validation`, and `openapi` pages for the full contract.
277
+
183
278
  ## Import styles
184
279
 
185
280
  The package ships both a default export and named exports: