@web-ts-toolkit/access-router 0.44.0 → 0.46.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/llms.txt CHANGED
@@ -43,6 +43,24 @@ const validator = fromZod(z.object({ name: z.string() }));
43
43
  const schema = defineRequestSchema(validator);
44
44
  ```
45
45
 
46
+ AJV (`fromAjv`, `AjvValidatorLike<T>` from the root export; install AJV in your app):
47
+ sync boolean verdicts use same-turn snapshotted `validate.errors`; true returns input,
48
+ false returns issues. `$async: true` means data-returning promises/thenables: every
49
+ fulfillment (false/true/null/scalars/objects) succeeds with that value. Async diagnostics
50
+ come only from rejection-carried AJV ValidationError (`ajv: true`, `validation: true`,
51
+ errors array), never shared errors; other exceptions preserve operational identity.
52
+ Untagged promises/thenables now fail with TypeError and are safely observed; sync
53
+ non-booleans also fail. Migration: retain the real compiled validator/tag, or use a
54
+ literal `$async: true as const` on structural data-returning validators; async boolean
55
+ verdict wrappers must switch to data/rejection semantics or RequestSchemaValidator
56
+ results. An errors property alone does not identify validation failure. Real AJV
57
+ overloads infer output without casts; fromAjv<T>(sync) specifies output for a structural
58
+ boolean validator without a type guard, structural async output is inferred from its
59
+ promise. AJV types can erase the async tag to their sync base interface,
60
+ so the runtime tag remains authoritative. Use an AJV `AsyncSchema`-typed schema for
61
+ compile/compileAsync to select its async overload; inline schemas may match AJV's
62
+ earlier sync overload even with `$async: true`. See README "AJV validation contract".
63
+
46
64
  Isolated runtime (no global state shared with default `acl`):
47
65
 
48
66
  <!-- doc-example: complete-runtime -->
@@ -78,6 +96,66 @@ const conservative: CopyAndDepopulateOutput = copyAndDepopulate({ items: [{ _id:
78
96
  void [typed, conservative];
79
97
  ```
80
98
 
99
+ Correlated includes: besides legacy `localField`/`foreignField` joins,
100
+ `include` entries accept `mode: 'correlated'` with `{ "$parent": "<field>" }`
101
+ markers in filter value positions (or as the whole `id`). Each entry runs per
102
+ parent against the immediate parent snapshot: reads attach doc-or-`null`,
103
+ lists attach per-parent paginated arrays, counts attach numbers (missing/null
104
+ refs attach `null`/`[]`/`0` with no query). Plain `'$special'` strings stay
105
+ literal; match literal `{ "$parent": "x" }` objects with
106
+ `{ "$escape": { "$parent": "x" } }`. A bare field escape resolves to literal
107
+ equality: `{ note: { $escape: { $parent: 'x' } } }` becomes
108
+ `{ note: { $eq: { $parent: 'x' } } }`. Explicit `$eq` stays equivalent;
109
+ operator operands and array elements stay literal values without double wrapping.
110
+ Resolution is single-pass and parent-independent, including missing/null parent
111
+ fields; malformed escapes are rejected and expanded complexity budgets still apply.
112
+ Older servers with the bare-escape defect need explicit `$eq`; upgrade the server
113
+ to use the corrected bare form. Identifier behavior, per-op target
114
+ authorization (denials fail the whole request with zero target queries), and
115
+ bounds (`maxIncludeCount`, revalidated `maxNodes`/`maxDepth`/`maxInValues`/
116
+ `maxLogicalClauses`, `maxCorrelatedQueries` default 100,
117
+ `maxCorrelatedDepth` default 5) follow the README "Correlated Includes"
118
+ section. Wire types (`ParentRef`, `CorrelatedInclude`, `Include`) are
119
+ available from `@web-ts-toolkit/access-router/advanced` without deep
120
+ imports.
121
+
122
+ Business boundary contracts (see the corresponding README sections):
123
+
124
+ - Include `path` is output-only. Legacy/correlated read/list/count paths cannot equal,
125
+ descend from, or contain the receiving model's `documentPermissionField` (default
126
+ `_permissions`), including equivalent legacy bracket paths. Nested receiving model
127
+ settings apply; preflight rejects before target persistence. Direct HTTP 400 / root
128
+ entry 400 `bad_request`; ordinary output collisions remain allowed.
129
+ - Nested updates patch authorized policy leaves, preserving omitted/protected siblings.
130
+ Whole-authorized objects/arrays replace; explicit indexed grants patch that leaf.
131
+ Send nested JSON, not literal dotted keys or Mongo update operators. Model prepare
132
+ output is trusted and not re-filtered; at partial policy ancestors omitted children
133
+ survive. Use explicit null/undefined to clear/unset, or trusted transform document
134
+ setters for whole replacement. Never blindly copy `context.originalData` back in.
135
+ - Subdocument create/update output requires post-save parent read access. Full-array
136
+ create also requires sub list AND read guards/row filters; targeted single/bulk
137
+ updates require sub read only. All use read fields plus `_id`, stored array order,
138
+ and visible counts. Hidden create: success/list/created, `data: [], count: 0`, HTTP
139
+ 201; bulk update: success/list/success, empty/0, HTTP 200; single update:
140
+ success/single/success, `data: null`, HTTP 200. Root entries use those statuses inside
141
+ HTTP 200. These are completed writes: do not retry merely for empty output.
142
+ - Legacy include counts use one grouped aggregate per include execution, exact distinct
143
+ authorized target-document counts beyond list limits/pagination, and target-schema
144
+ casting of ACL filters and foreign operands with original parent-key association.
145
+ Cast failures are BadRequest; terminal false count rows retain legacy omitted output.
146
+ Query setters/casting do not execute countDocuments middleware/plugins; arbitrary
147
+ aggregate pipelines are not auto-cast.
148
+ - `maxBulkConcurrency` (default 10) admits awaited adapter/document persistence per
149
+ request AND owning runtime, across root entries and nested includes; pools are
150
+ independent across requests/runtimes. Bulk create admits singleton arrays per item,
151
+ keeps result order and submits all items despite rejection. Recursive orchestration
152
+ holds no permit; limit 1 works. Root `maxConcurrentOperations` bounds whole operations
153
+ per order group separately. Permits release on errors without refunding cumulative
154
+ `maxCorrelatedQueries` (100); `maxCorrelatedDepth` (5) is a separate nesting bound.
155
+ One admitted operation can issue multiple driver commands through Mongoose middleware
156
+ or populate. This is not a global connection limit, throughput guarantee, transaction,
157
+ or governance of direct DB/network calls in trusted application hooks.
158
+
81
159
  ## Gotchas
82
160
 
83
161
  - peer dependencies: `express >= 5` and `mongoose >= 8`
@@ -85,15 +163,18 @@ void [typed, conservative];
85
163
  - `acl.createOpenApiRouter(options)` uses the default runtime; the standalone `createOpenApiRouter(runtime, options)` export requires the runtime as the first argument
86
164
  - `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
87
165
  - isolated runtimes do not silently read process-global `mongoose.models`; register the model first or pass the `mongoose.Model` instance to `runtime.createRouter(...)`
88
- - `/advanced` does NOT export `acl`, `defaultRuntime`, `createRouter`, or `createAccessRuntime` — those live on the root entry only
166
+ - `/advanced` exports interfaces/wire types, symbols, enums and request validation helpers, not runtime-context functions or runtime instances/factories. Use root `createAccessRuntime`/`AccessRuntime`/`defaultRuntime`, or `acl.createRouter(...)` for the default runtime.
89
167
  - `/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.
90
168
  - `createDataRouter(...)` holds data in-memory; use it for lightweight/non-persisted resources
91
169
  - 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.
170
+ - Model service `args.overrides.filter` (findOne/find/updateOne/upsert) and `args.overrides.idFilter` (findById/updateById): false returns Forbidden without fallback filter/ID generation or persistence, including client subqueries. Nullish uses normal generation; trusted object filters replace generated policy, while object idFilters still receive row policy.
171
+ - `Service.exists`: terminal explicit/base/override false now returns a Forbidden ErrorResult before adapter dispatch in both includeId modes (previously successful false/null). Allowed matches/misses remain true/false, or `{ _id }`/null with includeId. Check success before data.
172
+ - Public `_read`/`_readFilter` tryList never retries Forbidden/BadRequest. Authorized misses retain list fallback and its operation guard. `_upsert` with _id propagates denied exists as Forbidden (previously Unauthorized); allowed miss remains Unauthorized, with no create in either case. Generated direct/root-entry denial status is 403 (root envelope 200). Custom Express routes must map ErrorResult codes to HTTP status themselves; res.json does not do this automatically.
92
173
  - 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.
93
174
  - 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.
94
175
  - `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
95
176
 
96
177
  ## Pointers
97
178
 
98
- - README: installation, quickstart (Express + Mongoose end-to-end), main exports, runtime isolation, createRouter overloads
179
+ - README: installation, quickstart, exports, runtime isolation, include safety/counts, nested updates/trusted hooks, hidden subdocument responses, persistence limits and compatibility notes
99
180
  - website/docs/packages/access-router/: full documentation (routing, configuration, hooks, validation, openapi)
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@web-ts-toolkit/access-router",
3
3
  "description": "ACL-aware Express routers and in-memory data services for Mongoose-backed APIs",
4
4
  "homepage": "https://web-ts-toolkit.pages.dev/docs/packages/access-router",
5
- "version": "0.44.0",
5
+ "version": "0.46.0",
6
6
  "sideEffects": [
7
7
  "./**/index.js",
8
8
  "./**/index.mjs",
@@ -57,8 +57,8 @@
57
57
  "node": ">=22"
58
58
  },
59
59
  "dependencies": {
60
- "@web-ts-toolkit/express-json-router": "0.44.0",
61
- "@web-ts-toolkit/utils": "0.44.0",
60
+ "@web-ts-toolkit/express-json-router": "0.46.0",
61
+ "@web-ts-toolkit/utils": "0.46.0",
62
62
  "just-diff": "^6.0.2",
63
63
  "mongoose-schema-jsonschema": "^4.0.1",
64
64
  "sift": "^17.1.3",