@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/README.md +323 -2
- package/advanced.d.mts +655 -19
- package/advanced.d.ts +655 -19
- package/advanced.js +362 -22
- package/advanced.mjs +364 -23
- package/index.d.mts +5 -186
- package/index.d.ts +5 -186
- package/index.js +1783 -263
- package/index.mjs +1702 -178
- package/llms.txt +83 -2
- package/package.json +3 -3
- package/{parsers-B5dbVmjm.d.mts → parsers-BKF7VX2-.d.mts} +417 -13
- package/{parsers-B5dbVmjm.d.ts → parsers-BKF7VX2-.d.ts} +417 -13
- package/processors.d.mts +151 -1
- package/processors.d.ts +151 -1
- package/processors.js +170 -6
- package/processors.mjs +165 -6
package/README.md
CHANGED
|
@@ -110,6 +110,29 @@ filter before the base filter is applied, but it is not called for an existing `
|
|
|
110
110
|
that denial. Returning `false` from `overrideFilter` also denies the query. Empty or absent filters retain their
|
|
111
111
|
normal unrestricted meaning unless a base filter restricts them.
|
|
112
112
|
|
|
113
|
+
**Model service denial contract:** internal `findOne`/`find`/`updateOne`/`upsert`
|
|
114
|
+
`args.overrides.filter` and `findById`/`updateById` `args.overrides.idFilter` distinguish
|
|
115
|
+
`false` from omitted/`undefined`/`null`. False returns a `forbidden` ErrorResult without
|
|
116
|
+
fallback filter/identifier generation or persistence (including client subqueries).
|
|
117
|
+
Nullish overrides use normal generation. Trusted object filter overrides replace the
|
|
118
|
+
generated filter; object **idFilter** overrides replace identifier resolution but still
|
|
119
|
+
receive row policy. These service arguments are trusted application inputs.
|
|
120
|
+
|
|
121
|
+
`Service.exists(filter, { access: 'read', includeId: false })` returns a successful
|
|
122
|
+
boolean for allowed queries; `includeId: true` returns an `{ _id }` record or `null`.
|
|
123
|
+
Explicit/base/override terminal false now returns `forbidden` **before adapter dispatch**
|
|
124
|
+
in both modes, rather than a successful false/null. An allowed filter that matches no
|
|
125
|
+
visible row is still a successful miss. Check `result.success` before using `result.data`.
|
|
126
|
+
|
|
127
|
+
Public `_read` and `_readFilter` with `tryList: true` (the default) never retry Forbidden
|
|
128
|
+
or BadRequest. An authorized read miss can still fall back to list policy, subject to
|
|
129
|
+
list operation access. This deliberately removes list fallback for terminal read denial.
|
|
130
|
+
Public `_upsert` with an `_id` propagates denied update-policy existence as Forbidden
|
|
131
|
+
(previously Unauthorized); an allowed miss remains Unauthorized and neither case creates.
|
|
132
|
+
Generated direct routes map Forbidden to HTTP 403; root batches use per-entry 403 within
|
|
133
|
+
HTTP 200. **Custom Express routes own HTTP serialization:** returning an ErrorResult or
|
|
134
|
+
calling `res.json(result)` does not automatically set HTTP 403; map its code explicitly.
|
|
135
|
+
|
|
113
136
|
Advanced mutation validators drive dispatch: nested `data` validators run first, then the whole-body (`default`)
|
|
114
137
|
validator sees that transformed envelope, and the service persists only the final parsed `data`, `select`,
|
|
115
138
|
`populate`, `tasks`, and allowed `options`. Missing body options fall back to `returning_all`/`include_permissions`
|
|
@@ -126,12 +149,67 @@ Root entrypoint (`@web-ts-toolkit/access-router`):
|
|
|
126
149
|
- `guard(...)` and `GuardModelCondition`, `GuardModelConditionID` types
|
|
127
150
|
- `combineRoutes(...)` and `createOpenApiRouter(runtime, options)` (note: prefer `acl.createOpenApiRouter(options)` for the default runtime)
|
|
128
151
|
- validation adapters: `defineRequestSchema(...)`, `fromZod(...)`, `fromYup(...)`, `fromJoi(...)`, `fromAjv(...)`, `fromStandardSchema(...)`, `fromValibot(...)`, `fromArkType(...)`, `fromIoTs(...)`, `fromSuperstruct(...)`, `fromVine(...)`
|
|
152
|
+
- `permissionsPlugin` (src/plugins.ts) — Mongoose schema plugin used as `schema.plugin(permissionsPlugin, { modelName })`
|
|
153
|
+
- logger helpers: `redactFilter`, `redactPayload`, `safeStringify`, `isLevelEnabled` + `OpLogContext` type
|
|
129
154
|
- option helpers: `setGlobalOptions`, `setGlobalOption`, `getGlobalOptions`, `getGlobalOption`, `setModelOptions`, `setModelOption`, `getModelOptions`, `getModelOption`, `getModelNames`, `getModelJsonSchema`, `setDefaultModelOptions`, `setDefaultModelOption`, `getDefaultModelOptions`, `getDefaultModelOption`
|
|
130
155
|
|
|
131
156
|
Subpath entrypoints:
|
|
132
157
|
|
|
133
|
-
- `@web-ts-toolkit/access-router/advanced` —
|
|
134
|
-
- `@web-ts-toolkit/access-router/processors` — `copyAndDepopulate` and
|
|
158
|
+
- `@web-ts-toolkit/access-router/advanced` — request/service/option interfaces and include wire types, symbols (`MIDDLEWARE`, `DATA_MIDDLEWARE`, `PERMISSIONS`, ...), enums (`Codes`, `StatusCodes`), and validation helpers (`parseBody`, `parseQuery`, `parseBodyWithSchema`, request schemas). Runtime-context functions are internal; use root exports such as `createAccessRuntime`, `AccessRuntime`, and `defaultRuntime` for runtime ownership. `/advanced` does not export runtime instances or router-creation helpers.
|
|
159
|
+
- `@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.
|
|
160
|
+
|
|
161
|
+
## AJV validation contract
|
|
162
|
+
|
|
163
|
+
Install `ajv` in your application when using `fromAjv`; it is not a mandatory
|
|
164
|
+
access-router runtime dependency. Import the helper and `AjvValidatorLike` type
|
|
165
|
+
from `@web-ts-toolkit/access-router`.
|
|
166
|
+
|
|
167
|
+
- Synchronous validators return boolean **verdicts**: true returns the input
|
|
168
|
+
(including any in-place AJV mutations), false returns normalized issues. Mutable
|
|
169
|
+
`validate.errors` are snapshotted before any suspension or next validation call.
|
|
170
|
+
- Validators tagged `$async: true` return promises/thenables of **validated data**.
|
|
171
|
+
Every fulfillment succeeds, including `false`, `true`, `null`, scalars and objects;
|
|
172
|
+
the fulfilled value is returned, not substituted with the input. Shared
|
|
173
|
+
`validate.errors` is never read on this path. Real AJV `ValidationError` rejections
|
|
174
|
+
(`ajv: true`, `validation: true`, an `errors` array) provide input-local diagnostics
|
|
175
|
+
under concurrency. Other exceptions propagate unchanged as operational failures.
|
|
176
|
+
- Untagged promises/thenables now reject with a configuration `TypeError`; their
|
|
177
|
+
rejections are observed to avoid unhandled rejection. Sync non-boolean results
|
|
178
|
+
also reject with `TypeError`.
|
|
179
|
+
|
|
180
|
+
<!-- doc-example: complete-runtime -->
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { Ajv, type AsyncSchema } from 'ajv';
|
|
184
|
+
import { fromAjv } from '@web-ts-toolkit/access-router';
|
|
185
|
+
|
|
186
|
+
const ajv = new Ajv();
|
|
187
|
+
const sync = fromAjv<boolean>(ajv.compile<boolean>({ type: 'boolean' }));
|
|
188
|
+
const asyncSchema: AsyncSchema = { $async: true, type: 'boolean' };
|
|
189
|
+
const asyncData = fromAjv(ajv.compile<boolean>(asyncSchema));
|
|
190
|
+
const syncResult = await sync(false);
|
|
191
|
+
const asyncResult = await asyncData(false);
|
|
192
|
+
if (!syncResult.success || syncResult.data !== false || !asyncResult.success || asyncResult.data !== false) {
|
|
193
|
+
throw new Error('Both AJV validators must preserve valid false data');
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Migration:** pass genuine compiled AJV sync/async validators directly, without
|
|
198
|
+
casts or wrappers that discard `$async`. `AjvValidatorLike<T>` now separates sync
|
|
199
|
+
boolean and tagged async data signatures; use a literal tag (`$async: true as const`)
|
|
200
|
+
for structural async adapters. An async boolean-verdict wrapper must instead return
|
|
201
|
+
the original validated data or reject with an AJV validation error; merely adding
|
|
202
|
+
the tag changes false/true into successful data. Alternatively implement a
|
|
203
|
+
`RequestSchemaValidator` returning `{ success, data }` / `{ success: false, issues }`.
|
|
204
|
+
Rejections with only an `errors` property are operational, not AJV validation errors.
|
|
205
|
+
AJV's overloaded `AsyncValidateFunction<T>` extends its sync `ValidateFunction<T>`;
|
|
206
|
+
TypeScript can erase the tag in that base type, so runtime checks remain necessary.
|
|
207
|
+
Genuine AJV sync/async output types are inferred via their type-guard overload;
|
|
208
|
+
structural async output is inferred from the promise. Use `fromAjv<T>(syncValidator)`
|
|
209
|
+
for a structural boolean validator without a type guard. An inline schema can select AJV's earlier
|
|
210
|
+
sync overload even with `$async: true`; an `AsyncSchema`-typed schema, as above,
|
|
211
|
+
selects its async overload without casts (also for `compileAsync`). Runtime
|
|
212
|
+
behavior follows the validator's actual tag in either case.
|
|
135
213
|
|
|
136
214
|
## Default runtime vs. isolated runtime
|
|
137
215
|
|
|
@@ -200,6 +278,249 @@ Advanced read/list requests can attach related model data with `include` entries
|
|
|
200
278
|
`op: 'count'` returns exact authorized counts and ignores include pagination fields such as `args.limit`.
|
|
201
279
|
`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.
|
|
202
280
|
|
|
281
|
+
Legacy count joins use one grouped aggregate for all parents per include execution.
|
|
282
|
+
They count distinct target documents in the authorized count scope: duplicate local
|
|
283
|
+
keys, repeated foreign array entries, or one document matching multiple local keys
|
|
284
|
+
do not inflate a parent's count. No matches produce `0`.
|
|
285
|
+
|
|
286
|
+
The target Mongoose schema casts both the complete authorized filter (including
|
|
287
|
+
tenant filters) and foreign-key operands, including after array unwind. ObjectId/String
|
|
288
|
+
joins and query setters retain association to the original parent keys. Invalid
|
|
289
|
+
schema operands/filters produce controlled `BadRequest` (direct HTTP 400; root entry
|
|
290
|
+
`statusCode: 400`, `code: 'bad_request'`) rather than misleading zero counts. Count
|
|
291
|
+
operation denials still prevent target work; a terminal `false` count row policy
|
|
292
|
+
retains legacy omitted include output and a grouped-service Forbidden result.
|
|
293
|
+
|
|
294
|
+
This is query-schema/setter parity, not execution of `countDocuments` middleware or
|
|
295
|
+
plugins: casting is synchronous and performs no query. Aggregate middleware still
|
|
296
|
+
belongs to the aggregate operation. Express router count authorization through
|
|
297
|
+
`operationAccess` and row-filter hooks. Arbitrary pipelines passed to
|
|
298
|
+
`router.model.aggregate(...)` are not automatically cast.
|
|
299
|
+
|
|
300
|
+
### Safe include output paths
|
|
301
|
+
|
|
302
|
+
Treat `path` as output only: put an invoice's related count at `lineItemCount`, for
|
|
303
|
+
example, rather than inside permission metadata. Legacy and correlated read/list/count
|
|
304
|
+
paths must not equal, descend from, or be an ancestor of the receiving model's
|
|
305
|
+
`documentPermissionField` (default `_permissions`). With `auth.policy.permissions`,
|
|
306
|
+
`auth`, `auth.policy`, and `auth.policy.permissions.canReadSecret` all reject;
|
|
307
|
+
`auth.policy.permissionsExtra` is an ordinary sibling. Equivalent legacy bracket
|
|
308
|
+
paths follow the same rule.
|
|
309
|
+
|
|
310
|
+
The include tree is preflighted before target persistence, including every supported
|
|
311
|
+
nested level using that receiving target's configuration. Overlaps return BadRequest
|
|
312
|
+
(direct HTTP 400; affected root entry 400/`bad_request` within its HTTP 200 batch
|
|
313
|
+
envelope), even if the relationship would be empty. Malformed reads do not retry
|
|
314
|
+
through list access. Ordinary output-field replacement remains supported; generated
|
|
315
|
+
include data cannot grant source-field permission.
|
|
316
|
+
|
|
317
|
+
## Correlated Includes
|
|
318
|
+
|
|
319
|
+
Besides the legacy `localField`/`foreignField` joins, `include` entries accept
|
|
320
|
+
a correlated variant (`mode: 'correlated'`) that runs a target query **per
|
|
321
|
+
parent document** using values from that parent. References are explicit
|
|
322
|
+
structural markers — `{ "$parent": "<field>" }`, dotted paths allowed —
|
|
323
|
+
recognized only in filter _value_ positions (bare values, field operators
|
|
324
|
+
such as `$eq`/`$in`, `$in` array elements, `$and`/`$or`/`$nor` clauses,
|
|
325
|
+
`$elemMatch` subtrees) and as the whole `id` of identifier reads. Plain
|
|
326
|
+
strings are never references: `'$special'` keeps its literal query meaning.
|
|
327
|
+
Match a literal `{ "$parent": "x" }` object with
|
|
328
|
+
`{ "$escape": { "$parent": "x" } }`. In a bare field position,
|
|
329
|
+
`{ "note": { "$escape": { "$parent": "x" } } }` resolves to
|
|
330
|
+
`{ "note": { "$eq": { "$parent": "x" } } }`, just like bare substituted
|
|
331
|
+
parent objects. The explicit spelling
|
|
332
|
+
`{ "note": { "$eq": { "$escape": { "$parent": "x" } } } }` remains
|
|
333
|
+
equivalent. Operator operands and array elements receive the literal object
|
|
334
|
+
without another `$eq` wrapper. Escapes are resolved once and never read parent
|
|
335
|
+
data; missing, null or changed `parent.x` cannot change the literal. Malformed
|
|
336
|
+
escapes are rejected, and expanded operands still consume complexity budgets.
|
|
337
|
+
This bare-position correction requires an updated server; older servers with
|
|
338
|
+
the bare-escape defect still require the explicit `$eq` spelling.
|
|
339
|
+
|
|
340
|
+
Wire shapes (the originating method fixes `op`; there is no override):
|
|
341
|
+
|
|
342
|
+
```json
|
|
343
|
+
{ "mode": "correlated", "model": "Org", "op": "read", "path": "org", "id": { "$parent": "orgId" } }
|
|
344
|
+
{ "mode": "correlated", "model": "Org", "op": "read", "path": "org",
|
|
345
|
+
"filter": { "_id": { "$parent": "orgId" }, "active": true }, "args": { "select": ["name"] } }
|
|
346
|
+
{ "mode": "correlated", "model": "Post", "op": "list", "path": "posts",
|
|
347
|
+
"filter": { "authorId": { "$parent": "_id" }, "title": "$special" },
|
|
348
|
+
"args": { "select": ["title"], "sort": { "createdAt": -1 }, "limit": 5 } }
|
|
349
|
+
{ "mode": "correlated", "model": "Post", "op": "count", "path": "postCount",
|
|
350
|
+
"filter": { "authorId": { "$parent": "_id" } } }
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Semantics:
|
|
354
|
+
|
|
355
|
+
- References resolve against the **immediate parent** document as fetched
|
|
356
|
+
from persistence (including internally loaded reference fields), before
|
|
357
|
+
include attachment, `decorate` hooks, and task mutation. Nested
|
|
358
|
+
`args.include` entries bind to the target doc of the enclosing include.
|
|
359
|
+
Sibling include output never feeds references.
|
|
360
|
+
- A missing (`undefined`) or `null` reference short-circuits that parent's
|
|
361
|
+
execution to the no-match shape with no target query: `read` attaches
|
|
362
|
+
`null`, `list` attaches `[]`, `count` attaches `0`. Target misses produce
|
|
363
|
+
the same shapes. The output path is always set.
|
|
364
|
+
- Identifier reads use the target's configured identifier behavior
|
|
365
|
+
(`resolveIdentifierFilter`/`genIDFilter`, custom `resolveIdFilter`
|
|
366
|
+
honored) with no read-to-list fallback. Counts use explicit count access
|
|
367
|
+
with count semantics, independent of any list limit. List pagination
|
|
368
|
+
(`skip`/`limit`/`page`/`pageSize`) applies **per parent** — contrast with
|
|
369
|
+
legacy batched list includes, whose pagination applies across the parent
|
|
370
|
+
set. Substituted parent values are data: objects match literally (never
|
|
371
|
+
become operators) and arrays are never flattened.
|
|
372
|
+
- Each correlated entry counts toward `maxIncludeCount`; expanded filters
|
|
373
|
+
are revalidated against `maxNodes`/`maxDepth`/`maxInValues`/
|
|
374
|
+
`maxLogicalClauses`. Total inner executions per request/runtime are bounded by
|
|
375
|
+
`maxCorrelatedQueries` (default `100`) and nesting by
|
|
376
|
+
`maxCorrelatedDepth` (default `5`); exceeding any bound fails the whole
|
|
377
|
+
request/affected root entry. Fan-out shares request-and-runtime-owned
|
|
378
|
+
persistence admission (see below). Target authorization denials fail the
|
|
379
|
+
request/affected root entry with zero target persistence queries; target
|
|
380
|
+
runtime errors also fail that request/entry.
|
|
381
|
+
`path` must be a non-empty valid field path (not `_id`, not `$`-prefixed);
|
|
382
|
+
duplicates in one include array are `BadRequest`; collisions with parent
|
|
383
|
+
fields overwrite except for the permission-metadata overlaps described above.
|
|
384
|
+
- Referenced parent fields are added to the parent DB select like legacy
|
|
385
|
+
`localField`s, then trimmed unless allowed by field policy and selected.
|
|
386
|
+
A policy-forbidden reference still resolves — only its query effects are
|
|
387
|
+
visible, never the value.
|
|
388
|
+
- Direct count parents carry no `include`; correlated _count_ includes
|
|
389
|
+
attach to read/list parents only. Correlated entries are accepted in both
|
|
390
|
+
direct (`model-router.ts`) and root (`root-router.ts`) validators with
|
|
391
|
+
identical verdicts; malformed correlated input is a controlled
|
|
392
|
+
`BadRequest`, never a silent drop. Legacy entries also enforce safe output paths, and
|
|
393
|
+
legacy-shaped entries carrying `$parent` markers are rejected.
|
|
394
|
+
|
|
395
|
+
The TypeScript wire types (`ParentRef`, `CorrelatedInclude`, `Include`) are
|
|
396
|
+
reachable without deep imports via `@web-ts-toolkit/access-router/advanced`
|
|
397
|
+
(which re-exports `./interfaces`). See the website `services`,
|
|
398
|
+
`configuration`, `validation`, and `openapi` pages for the full contract.
|
|
399
|
+
|
|
400
|
+
## Nested updates and trusted hooks
|
|
401
|
+
|
|
402
|
+
Partial updates follow **authorized policy paths**, not a generic deep merge. For a
|
|
403
|
+
customer profile editor, grant only the editable leaf and send nested JSON:
|
|
404
|
+
|
|
405
|
+
<!-- doc-example: partial -->
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
import type { ModelRouterOptions } from '@web-ts-toolkit/access-router';
|
|
409
|
+
|
|
410
|
+
// Non-generic options accept dotted policy paths.
|
|
411
|
+
const options: ModelRouterOptions = {
|
|
412
|
+
operationAccess: { update: true },
|
|
413
|
+
permissionSchema: { 'profile.public': { update: true } },
|
|
414
|
+
};
|
|
415
|
+
const updateBody = { profile: { public: 'New display name' } };
|
|
416
|
+
// Pass options to acl.createRouter(CustomerModel, options); send updateBody to PATCH.
|
|
417
|
+
// Existing profile.secret and omitted siblings survive.
|
|
418
|
+
void [options, updateBody];
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
This applies to `updateOne`, `updateById`, existing-row upsert, and single/bulk
|
|
422
|
+
subdocument updates, including Mongoose nested and single-nested schemas. Authorized
|
|
423
|
+
leaves can initialize missing/null containers. Authorizing `profile` itself instead
|
|
424
|
+
allows whole replacement and removes omitted children; whole grants take priority
|
|
425
|
+
over overlapping leaf grants. An authorized object leaf replaces just that leaf.
|
|
426
|
+
Arrays replace as a whole when authorized as a whole; an explicit `items.0.public`
|
|
427
|
+
grant patches that indexed leaf. Omitted fields remain unchanged, admitted `null`
|
|
428
|
+
clears the selected field, and authorized empty objects/arrays replace it (Mongoose
|
|
429
|
+
`minimize` may omit empty objects in storage). A null/empty/malformed client container
|
|
430
|
+
does not supply descendant leaves and cannot clear a protected parent. Casting and
|
|
431
|
+
validation still apply. Literal dotted/bracket client keys are ignored by field
|
|
432
|
+
selection; they are not `$set`/`$unset` instructions.
|
|
433
|
+
|
|
434
|
+
Model `prepare` and `transform` hooks are trusted application code. Validation sees
|
|
435
|
+
selected client data before prepare. Prepare output is not re-filtered: it can add
|
|
436
|
+
protected/server-only fields, including dotted Mongoose paths. At strict ancestors of
|
|
437
|
+
admitted paths, supplied children of plain-object/array prepare output are applied
|
|
438
|
+
while omitted siblings survive. Other prepare paths remain whole assignments.
|
|
439
|
+
An empty partial container or omitted parent does not delete siblings; explicit
|
|
440
|
+
`{ profile: null }` / `{ profile: undefined }` can clear/unset it. An entirely
|
|
441
|
+
null/undefined prepare result supplies no assignments.
|
|
442
|
+
|
|
443
|
+
For deliberate parent replacement, trusted `transform.update` can call
|
|
444
|
+
`doc.set('profile', replacement)`. Do not blindly reintroduce unfiltered
|
|
445
|
+
`context.originalData`. The update order remains validation → prepare →
|
|
446
|
+
assigned-document transform → save → afterPersist → changes; snapshots/diffs reflect
|
|
447
|
+
persisted changes. Subdocument operations retain their own lifecycle and do not gain
|
|
448
|
+
model prepare/transform hooks.
|
|
449
|
+
|
|
450
|
+
## Subdocument writes with hidden responses
|
|
451
|
+
|
|
452
|
+
Appending an attachment or updating a line item can succeed without permission to
|
|
453
|
+
read it. After saving, mutation responses require the parent's read operation/row
|
|
454
|
+
access, checked against post-save state. Full-array **create** responses (including
|
|
455
|
+
POST `[]`) additionally require both `subs.<sub>.list` and `.read` operation guards
|
|
456
|
+
and row filters. **Single/bulk update** responses require subdocument read access
|
|
457
|
+
only and return readable mutation targets; they do not require list access.
|
|
458
|
+
|
|
459
|
+
All three use read-field projection plus `_id`. Counts reflect visible returned
|
|
460
|
+
rows, not stored/updated totals. Filtering preserves stored array order, including
|
|
461
|
+
service `addFirst` insertion order; bulk request order does not reorder the response.
|
|
462
|
+
`addFirst` remains a service option, not a new generated HTTP option.
|
|
463
|
+
|
|
464
|
+
When a response guard denies access, a row policy returns `false`, or no readable
|
|
465
|
+
parent/rows match, the committed write still succeeds:
|
|
466
|
+
|
|
467
|
+
| Operation | Service/root result fields | Direct HTTP body/status; root entry status |
|
|
468
|
+
| ------------- | ------------------------------------------------------------------ | ------------------------------------------ |
|
|
469
|
+
| Create | `success: true, kind: 'list', code: 'created', data: [], count: 0` | `[]`, 201; 201 |
|
|
470
|
+
| Bulk update | `success: true, kind: 'list', code: 'success', data: [], count: 0` | `[]`, 200; 200 |
|
|
471
|
+
| Single update | `success: true, kind: 'single', code: 'success', data: null` | JSON `null`, 200; 200 |
|
|
472
|
+
|
|
473
|
+
Root batches retain their HTTP 200 envelope. **Do not retry a successful write just
|
|
474
|
+
because its output is empty.** Actual write denials and validation/persistence failures
|
|
475
|
+
retain their errors. Response visibility needs at most one extra parent query and
|
|
476
|
+
resolves applicable row policies once per mutation, rather than once per returned row.
|
|
477
|
+
|
|
478
|
+
## Persistence concurrency and resource limits
|
|
479
|
+
|
|
480
|
+
`requestComplexity.maxBulkConcurrency` (default `10`) bounds simultaneous awaited
|
|
481
|
+
**adapter/document persistence operations** for access-router model services sharing
|
|
482
|
+
one request and runtime. Direct service calls, root entries, subqueries, legacy
|
|
483
|
+
includes and nested correlated includes share admission. Different requests and
|
|
484
|
+
different runtimes have independent pools and correlated-query totals. Services
|
|
485
|
+
capture their owning runtime at construction; later ambient runtime changes do not
|
|
486
|
+
move their persistence to another pool.
|
|
487
|
+
|
|
488
|
+
Admission covers lazy adapter query settlement, document save/delete, and
|
|
489
|
+
service-triggered post-write populate. Bulk create submits one admitted singleton-array
|
|
490
|
+
adapter call per item, preserves result order, and still submits every item if one
|
|
491
|
+
rejects. Internal custom adapters must preserve array-in/array-out create semantics.
|
|
492
|
+
Their optional synchronous `castFilter` must cast a copy and preserve `$in` operand
|
|
493
|
+
order/cardinality; without it they retain identity casting.
|
|
494
|
+
|
|
495
|
+
Recursive orchestration holds no persistence permit while waiting for descendants,
|
|
496
|
+
so limit `1` supports nested includes. Internal map/run loops remain bounded per
|
|
497
|
+
invocation. `RootRouter.maxConcurrentOperations` separately bounds whole root
|
|
498
|
+
operations within each order group, with results in input-index order. Permits
|
|
499
|
+
release on success or error and pass FIFO to waiting work; release neither cancels
|
|
500
|
+
submitted siblings nor refunds the cumulative `maxCorrelatedQueries` budget (default
|
|
501
|
+
`100`). `maxCorrelatedDepth` (default `5`) separately bounds template nesting.
|
|
502
|
+
|
|
503
|
+
The ceiling is **not a MongoDB driver-command count**: one admitted operation can
|
|
504
|
+
run Mongoose middleware/populate that issues multiple commands. Router ACL, prepare,
|
|
505
|
+
afterPersist, decorate and recursive include orchestration are outside the permit;
|
|
506
|
+
Mongoose's own middleware runs inside the awaited operation. Direct database/network
|
|
507
|
+
calls from trusted hooks are not governed. This is neither a process-wide connection
|
|
508
|
+
pool limit nor a throughput guarantee. Controlled nested-query instrumentation reduced
|
|
509
|
+
peak active collection calls from 9 to 3 at limit 3 for the tested paths; it is not a
|
|
510
|
+
production benchmark or a guarantee about middleware/populate fan-out.
|
|
511
|
+
|
|
512
|
+
### Compatibility notes
|
|
513
|
+
|
|
514
|
+
These boundary fixes preserve public entrypoints and require no new dependency or
|
|
515
|
+
option. Move computed include output away from permission metadata. Nested leaf edits
|
|
516
|
+
now preserve protected siblings; prepare hooks relying on omission to delete a parent
|
|
517
|
+
must replace it explicitly in transform. Handle successful null/empty subdocument
|
|
518
|
+
responses as completed writes. Legacy counts now cast schema values and report invalid
|
|
519
|
+
operands as BadRequest; they remain grouped and independent of list pagination.
|
|
520
|
+
Persistence admission is shared across nested work within each request/runtime, so
|
|
521
|
+
timing may change. It does not provide transactions, idempotency or optimistic
|
|
522
|
+
concurrency protection for competing edits.
|
|
523
|
+
|
|
203
524
|
## Import styles
|
|
204
525
|
|
|
205
526
|
The package ships both a default export and named exports:
|