@web-ts-toolkit/access-router 0.47.1 → 0.49.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
@@ -26,6 +26,7 @@ const userRouter = acl.createRouter('User', { basePath: '/users' });
26
26
 
27
27
  const fruitRouter = acl.createDataRouter('fruit', {
28
28
  basePath: '/fruit',
29
+ idParam: 'id',
29
30
  idField: 'id',
30
31
  data: [{ id: 'apple', name: 'Apple' }],
31
32
  });
@@ -33,6 +34,140 @@ const fruitRouter = acl.createDataRouter('fruit', {
33
34
  const docsRouter = acl.createOpenApiRouter({ title: 'Example API', version: '1.0.0' });
34
35
  ```
35
36
 
37
+ ## Basic/advanced operation guards
38
+
39
+ Use root `OperationAccess` / `ModelRouterOptions` / `DataRouterOptions` types;
40
+ `Validation` = boolean | string | string[] | GuardHook. Root imports are canonical:
41
+ default `acl` for the shared runtime, named `createAccessRuntime` for isolation.
42
+ `PermissionSchema` / `FieldOperationAccess` grant fields by base operation, not variants.
43
+
44
+ <!-- doc-example: partial -->
45
+ ```ts
46
+ import acl from '@web-ts-toolkit/access-router';
47
+
48
+ // Register User before constructing this model router.
49
+ const router = acl.createRouter('User', {
50
+ basePath: '/users',
51
+ operationAccess: { list: true, read: true, basicRead: false, advancedList: 'isAdmin' },
52
+ permissionSchema: { name: true },
53
+ });
54
+ router.operationAccess('basicRead', false); // Live per-key update, not object replacement.
55
+ router.setOption('operationAccess.basicRead', undefined); // Inherit exact model default, then read.
56
+ router.operationAccess({ list: true, read: true, advancedList: false }); // Replace whole rule object.
57
+ ```
58
+
59
+ Model pairs (paths relative to basePath, default segments):
60
+
61
+ | Base | Basic key / endpoint | Advanced key / endpoint |
62
+ | --- | --- | --- |
63
+ | list | basicList: GET / | advancedList: POST /__query |
64
+ | read | basicRead: GET /:id | advancedRead: POST /__query/:id and /__query/__filter |
65
+ | create | basicCreate: POST / | advancedCreate: POST /__mutation |
66
+ | update | basicUpdate: PATCH /:id | advancedUpdate: PATCH /__mutation/:id |
67
+ | upsert | basicUpsert: PUT / | advancedUpsert: PUT /__mutation |
68
+ | count | basicCount: GET /count | advancedCount: POST /count |
69
+ | distinct | basicDistinct: GET /distinct/:field | advancedDistinct: POST /distinct/:field |
70
+
71
+ Data supports only four list/read keys and both advanced read paths; set idParam: 'id'
72
+ explicitly for the illustrated data identifier paths. Each model sub
73
+ field supports subs.<field>.basicList / advancedList / basicRead / advancedRead for
74
+ GET /:id/<field>, POST /:id/<field>/__query, GET /:id/<field>/:subId, and
75
+ POST /:id/<field>/:subId/__query respectively. GET /new, DELETE /:id and subdocument
76
+ create/delete/both PATCH routes keep new/delete/sub create/delete/update base guards.
77
+
78
+ - Exact live variant replaces the base; only undefined inherits base exact →
79
+ operationAccess.default → scalar shorthand. False/empty-array/false-returning hooks
80
+ are terminal. Strings AND space-separated names; arrays OR entries; async hooks use
81
+ request `this` and AccessRouterPermissions. Operational errors do not fall back.
82
+ - Model exact lookup includes the runtime-default same path before model base/default/
83
+ shorthand. Options assign shallowly; no recursive default merge or broadcast of new
84
+ defaults over stored model values. Clearing a stored variant may expose a live default
85
+ variant. getDefaultModelOption is exact, not alias/default resolution. Data has no
86
+ model defaults. Use operationAccess: true or { default: true } for broad base fallback.
87
+ - Sub precedence: exact field variant → field base → defined scalar/closed field
88
+ object → top variant only for an absent field → top base. { items: { read: true } }
89
+ closes items list even with top list: true. No nested .default fallback. Legacy scalar
90
+ subs is accepted but does not guard absent fields; use explicit per-field rules.
91
+ - Denial is HTTP 401, not route omission. getEndpoints()/OpenAPI retain denied routes.
92
+ HEAD follows its basic GET. { list: true, read: true, basicRead: false } denies only
93
+ GET-by-id/HEAD among top-level routes while both advanced reads work; sub fields use
94
+ their documented precedence. { list: true, advancedList: false }
95
+ denies only advanced list; other endpoint guards remain independent.
96
+ - Setters mutate live policy. Dotted paths are setter arguments, not flat constructor
97
+ properties. operationAccess(key, definedValue) updates one key; object forms replace
98
+ the entire operationAccess value shallowly. Use setOption(path, undefined) to clear.
99
+ router.options is a frozen construction snapshot; getter results are frozen snapshots.
100
+ Read current runtime values again; generic getModelOption/getDataOption is not the
101
+ route resolver (exact getters expose variant paths, including model-default fallback).
102
+ - Variants do not grant fields, row/filter/hook access, target access, or mutation-output
103
+ visibility. Model read misses/tryList still require base list; advancedList: true cannot
104
+ replace denied list, basicList: false cannot block base-list retry. Forbidden/BadRequest
105
+ never retry. Upsert branches do not require create/update route variants. Root entries
106
+ retain base op names/checks (entry 401 within HTTP 200); variant op names are invalid.
107
+ Trusted service calls retain existing checks, with no req.method variant inference.
108
+ - Wire populateAccess/descriptor access is only list/read (PopulateAccess from /advanced).
109
+ Shared Core populate rejects all fourteen reserved variants in effective options and
110
+ descriptors, including generic legacy include/subquery options, before target work.
111
+ Other trusted non-reserved custom strings keep runtime compatibility. BadRequest maps
112
+ to direct 400/root entry 400 bad_request; legacy read/list target BadRequest propagates
113
+ instead of silent omission. See README "Basic and advanced operation access" for the
114
+ full fallback examples, closed-field caveat, setters, and runnable data-router split.
115
+
116
+ Virtuals (computed fields): package-level async request-aware derived output
117
+ fields, reachable from the root and the existing `./advanced` barrel (no new
118
+ subpaths). One public `select` covers persisted + virtual fields; `dependsOn`
119
+ names are top-level persisted fields relative to the definition scope.
120
+ Selected virtual outputs are optional (`Partial`): authorization, absent
121
+ dependencies, `undefined`, and fail-closed throws can omit them. Virtuals
122
+ compute for lean and hydrated results after internal document permissions and
123
+ before `decorate`/`decorateAll`/tasks; internal-only dependencies are stripped
124
+ before those hooks. Every document-bearing output is covered (direct
125
+ list/read/create/update/upsert/new, populate/include targets incl. nested,
126
+ embedded `sub` scopes, subdocument routes) with target-model trimming on both
127
+ populate and include. Virtuals are output-only: never in persisted
128
+ projections, write admission, or DB sort/filter/distinct. `new()` ignores
129
+ `args.select`; select grammar is unchanged (arbitrary strings accepted).
130
+ Performance: prefer finalized populate/include data over per-row DB calls
131
+ (measured 24 rows: per-row `findById` = 25 driver reads vs populated getter =
132
+ 2 reads, same peak bound); `maxHookConcurrency` (default 10) bounds
133
+ concurrently awaited getter + row-finalization orchestration per
134
+ request/runtime only, not query counts. Parent getters see only finalized
135
+ related/embedded outputs, never stripped private fields. See README "Virtuals
136
+ (computed fields)" and "Virtuals compatibility notes" (populate trimming now
137
+ applies incl. no-virtual targets — intended change, not silent compat).
138
+
139
+ <!-- doc-example: partial -->
140
+ ```ts
141
+ import type { ModelRouterOptions } from '@web-ts-toolkit/access-router';
142
+ import type { SelectedPublicOutput } from '@web-ts-toolkit/access-router/advanced';
143
+
144
+ interface VirtUser {
145
+ name: string;
146
+ address: string;
147
+ }
148
+ interface VirtUserVirtuals {
149
+ fullAddress: string;
150
+ }
151
+
152
+ // Persisted VirtUser has no fullAddress; the virtual computes it.
153
+ const virtualOptions: ModelRouterOptions<VirtUser, VirtUserVirtuals> = {
154
+ permissionSchema: {
155
+ name: { read: true },
156
+ address: { read: 'canViewAddress' },
157
+ fullAddress: { read: 'canViewAddress' },
158
+ },
159
+ virtuals: {
160
+ fullAddress: {
161
+ dependsOn: ['address'],
162
+ read: async (doc) => (doc.address === undefined ? undefined : `addr:${doc.address}`),
163
+ },
164
+ },
165
+ };
166
+ type VirtualSelected = SelectedPublicOutput<VirtUser, ['name', 'fullAddress'], VirtUserVirtuals>;
167
+ const virtualPresent: VirtualSelected = { name: 'Ada', fullAddress: 'addr:a1' };
168
+ const virtualOmitted: VirtualSelected = { name: 'Ada' };
169
+ ```
170
+
36
171
  Validation adapters:
37
172
 
38
173
  <!-- doc-example: complete-runtime -->
@@ -171,10 +306,11 @@ Business boundary contracts (see the corresponding README sections):
171
306
  - `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
307
  - 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.
173
308
  - 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.
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.
309
+ - 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`/`includeFieldPermissions`/`populateAccess`, plus `returningAll` for update/upsert) reach the service. Missing body options fall back to `returning_all`/`include_permissions` query params. Simple GET routes also accept `include_field_permissions` query params.
310
+ - Router permission-output postures (absolute over client input and per-operation `defaults`): `stripPermissionsField: true` removes the whole permissions field from outputs (output matches `includePermissions: false` with the field, including its placeholder, deleted; enforcement still runs); `disableFieldPermissions: true` never computes `_view`/`_edit` maps while `includePermissions` still controls `docPermissions`-hook keys.
175
311
  - `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
176
312
 
177
313
  ## Pointers
178
314
 
179
- - README: installation, quickstart, exports, runtime isolation, include safety/counts, nested updates/trusted hooks, hidden subdocument responses, persistence limits and compatibility notes
315
+ - README: installation, quickstart, exports, operation-access matrix/fallback/live updates, runnable data split, runtime isolation, virtuals (computed fields) + access matrix + N+1/bounded-concurrency guidance + compatibility notes, include safety/counts, nested updates/trusted hooks, hidden subdocument responses, persistence limits and compatibility notes
180
316
  - 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.47.1",
5
+ "version": "0.49.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.47.1",
61
- "@web-ts-toolkit/utils": "0.47.1",
60
+ "@web-ts-toolkit/express-json-router": "0.49.0",
61
+ "@web-ts-toolkit/utils": "0.49.0",
62
62
  "just-diff": "^6.0.2",
63
63
  "mongoose-schema-jsonschema": "^4.0.1",
64
64
  "sift": "^17.1.3",