@web-ts-toolkit/access-router 0.48.0 → 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.
@@ -24,9 +24,128 @@ declare class Permission {
24
24
  type Permissions = Permission;
25
25
  type AccessRouterPermissions = Permission;
26
26
 
27
- declare class Service<TModel = unknown> extends Base<TModel> {
27
+ declare class RequestConcurrencyScheduler {
28
+ readonly limit: number;
29
+ private activeWork;
30
+ private readonly waiters;
31
+ constructor(limit: number);
32
+ map<TInput, TOutput>(items: TInput[], iteratee: (item: TInput, index: number, scheduled: true) => Promise<TOutput>): Promise<TOutput[]>;
33
+ run<TOutput>(iteratee: (scheduled: true) => Promise<TOutput>): Promise<TOutput>;
34
+ work<TOutput>(operation: () => TOutput | PromiseLike<TOutput>): Promise<TOutput>;
35
+ }
36
+ /**
37
+ * Shared hook-orchestration gate for VIRT-03 finalization (VIRT-00A D7).
38
+ *
39
+ * Distinct from {@link RequestConcurrencyScheduler.work} (leaf persistence
40
+ * permits): this gate admits virtual getter bodies only, never persistence
41
+ * ops. A single gate instance is shared across all rows of a top-level list
42
+ * so peak active getter work stays at or below the configured limit instead
43
+ * of multiplying by row count. Callers hold the gate only for the getter body
44
+ * itself — never while awaiting descendant finalization — so limit-1
45
+ * configurations complete without deadlock. FIFO admission keeps completion
46
+ * order independent of input order; the surrounding row map preserves stable
47
+ * index-keyed output association regardless of completion order.
48
+ */
49
+ declare class SharedHookGate {
50
+ readonly limit: number;
51
+ private active;
52
+ private readonly waiters;
53
+ constructor(limit: number);
54
+ run<TOutput>(operation: () => TOutput | PromiseLike<TOutput>): Promise<TOutput>;
55
+ }
56
+
57
+ /** Internal effective data policy, including trusted custom accesses; virtual configuration keys stay narrow. */
58
+ type VirtualProjectionAccess = 'list' | 'create' | 'read' | 'update' | (string & {});
59
+ type VirtualCandidateAuthState = 'granted' | 'deferred';
60
+ interface VirtualProjectionSnapshot {
61
+ permissionSchema?: Record<string, unknown> | null;
62
+ virtuals?: Record<string, unknown> | null;
63
+ /** Raw `alwaysSelectFields` option: array or per-access record. */
64
+ alwaysSelectFields?: string[] | Record<string, string[]> | null;
65
+ modelPermissionPrefix?: string;
66
+ requireExplicitSelect?: boolean;
67
+ documentPermissionField?: string;
68
+ }
69
+ interface PlanVirtualCandidate {
70
+ name: string;
71
+ descriptor: {
72
+ get: (...args: never[]) => unknown;
73
+ dependsOn: string[];
74
+ };
75
+ authState: VirtualCandidateAuthState;
76
+ dependsOn: string[];
77
+ virtualAccess: VirtualProjectionAccess;
78
+ outputAccess: VirtualProjectionAccess;
79
+ }
80
+ interface VirtualProjectionPlan {
81
+ receivingModelName: string;
82
+ scopePath: string[];
83
+ virtualAccess: VirtualProjectionAccess;
84
+ outputAccess: VirtualProjectionAccess;
85
+ docPermissionsAccess: VirtualProjectionAccess;
86
+ selectionMode: 'all' | 'include' | 'exclude';
87
+ /** Normalized effective select (empty = omitted/effectively empty). */
88
+ effectiveSelectNorm: string[];
89
+ /** Persisted output eligibility + virtual names eligible for output. */
90
+ outputSelection: string[];
91
+ outputPersistedFields: string[];
92
+ outputVirtualNames: string[];
93
+ /** Safe Mongoose projection: no virtuals, no `.sub` paths, no `-_id`. */
94
+ persistedFetchSelection: string[];
95
+ candidates: PlanVirtualCandidate[];
96
+ virtualOnlyDeps: string[];
97
+ internalOnlyFields: string[];
98
+ depProvenance: Record<string, string[]>;
99
+ outputIdExcluded: boolean;
100
+ fetchIdRetained: boolean;
101
+ internalIdentityFields: string[];
102
+ internalJoinFields: string[];
103
+ childPlans: Record<string, VirtualProjectionPlan>;
104
+ relatedPlans: VirtualProjectionPlan[];
105
+ }
106
+
107
+ type VirtualFinalizeAccess = VirtualProjectionAccess;
108
+ interface FinalizeSnapshot extends VirtualProjectionSnapshot {
109
+ documentPermissionField?: string;
110
+ exposedDocPermissionKeys?: string[] | undefined;
111
+ stripPermissionsField?: boolean;
112
+ disableFieldPermissions?: boolean;
113
+ }
114
+ interface FinalizeRelatedTarget {
115
+ /** Output path in the parent scope (top-level name for v1; dotted supported). */
116
+ path: string;
117
+ plan: VirtualProjectionPlan;
118
+ snapshot: FinalizeSnapshot;
119
+ virtualAccess: VirtualFinalizeAccess;
120
+ outputAccess: VirtualFinalizeAccess;
121
+ docPermissionsAccess: VirtualFinalizeAccess;
122
+ }
123
+
124
+ /**
125
+ * VIRT-04 direct-operation virtual wiring (VIRT-00A D1/D6/D7/D8).
126
+ *
127
+ * - Access matrix: list `list/list/list`; read/readFilter `read/read/read`
128
+ * (read using list fallback becomes `list/list/list` with
129
+ * `operation: 'read'` preserved); create/create-branch `create/read/create`;
130
+ * update/update-branch `update/read/update`; new template
131
+ * `create/create/create` (see `new()` note on ignored `args.select`).
132
+ * - Effective mutation output selection is carried on the internal plan
133
+ * into the shared finalizer before evaluation (explicit select,
134
+ * `returningAll: false` implicits, decorator/task selection preserved
135
+ * via existing public picks after decorate/tasks). Internal create/update
136
+ * args omit `select`; `overrides.effectiveSelect` is the deliberate
137
+ * transport (see `interfaces/service-create.ts`, `service-update.ts`).
138
+ * - Output-only admission runs before validation/persistence; computed
139
+ * values are assigned only to output copies (finalizer), never to
140
+ * persisted docs/snapshots.
141
+ * - `maxHookConcurrency` bounds active getter + row-finalization work via
142
+ * ONE bounded row map + shared `SharedHookGate` (stable order, limit-1
143
+ * completes, no persistence permits held).
144
+ */
145
+ type VirtualOperationAccess = 'list' | 'create' | 'read' | 'update';
146
+ declare class Service<TModel = unknown, TVirtuals extends object = Record<never, never>> extends Base<TModel, TVirtuals> {
28
147
  protected model: any;
29
- protected options: ModelRouterOptions<TModel>;
148
+ protected options: ModelRouterOptions<TModel, TVirtuals>;
30
149
  defaults: Defaults<TModel>;
31
150
  protected baseFields: string[];
32
151
  protected baseFieldsExt: string[];
@@ -38,7 +157,7 @@ declare class Service<TModel = unknown> extends Base<TModel> {
38
157
  lean: boolean;
39
158
  }): any;
40
159
  protected createModelAdapter(modelName: string): any;
41
- protected getModelRouterOptions(modelName: string): ModelRouterOptions<TModel>;
160
+ protected getModelRouterOptions(modelName: string): ModelRouterOptions<TModel, TVirtuals>;
42
161
  /**
43
162
  * Router-level permission output postures (see `ModelRouterOptions`).
44
163
  * Absolute: they win over client input and per-operation `defaults`.
@@ -61,6 +180,164 @@ declare class Service<TModel = unknown> extends Base<TModel> {
61
180
  * through untouched.
62
181
  */
63
182
  protected resolveEffectiveSelect(select: Projection | undefined): Projection | undefined;
183
+ /**
184
+ * Capture a coherent virtual snapshot for an operation (VIRT-00A D1/D2,
185
+ * VIRT-01 copy-on-write). Planner and finalizer use this same object for
186
+ * the operation so in-flight descriptor replacement cannot mix versions.
187
+ * Public for VIRT-06 subdocument routes (same coherence per sub op).
188
+ */
189
+ captureVirtualSnapshot(): FinalizeSnapshot;
190
+ getVirtualGlobalPermissions(): {
191
+ has: (key: string) => boolean;
192
+ hasKey: (key: string) => boolean;
193
+ };
194
+ getVirtualHookConcurrencyLimit(): number;
195
+ buildVirtualPlan(args: {
196
+ snapshot: FinalizeSnapshot;
197
+ virtualAccess: VirtualOperationAccess;
198
+ outputAccess: VirtualOperationAccess;
199
+ docPermissionsAccess: VirtualOperationAccess;
200
+ requestedSelect?: Projection;
201
+ effectiveSelect?: Projection;
202
+ internalFetch?: {
203
+ baseFields?: string[];
204
+ joinFields?: string[];
205
+ identityFields?: string[];
206
+ extraPersistedFields?: string[];
207
+ trustedOverrideSelect?: Projection;
208
+ };
209
+ }): VirtualProjectionPlan;
210
+ /**
211
+ * VIRT-06 sub-scope planner.
212
+ *
213
+ * Builds a `planVirtualProjection` with `scopePath=[sub,'sub']` for
214
+ * dedicated subdocument routes (`virtuals.<field>.sub.<name>.<access>`).
215
+ * `dependsOn` resolves relative to that scope (VIRT-00A D5); literal
216
+ * `.sub` segments never become DB/response names (planner strips them).
217
+ * Uses the same coherent `snapshot` captured once per sub op.
218
+ */
219
+ buildSubVirtualPlan(sub: string, snapshot: FinalizeSnapshot, virtualAccess: VirtualOperationAccess, outputAccess: VirtualOperationAccess, docPermissionsAccess: VirtualOperationAccess, requestedSelect?: Projection, effectiveSelect?: Projection): VirtualProjectionPlan;
220
+ /**
221
+ * VIRT-06 internal accessors for dedicated subdocument routes.
222
+ * `model-subdocument-service` uses these instead of reaching into
223
+ * protected `req`/`modelName`/`model`, preserving raw docs for
224
+ * persistence (never substituting trimmed DTOs) and keeping request
225
+ * ownership inside `Service`.
226
+ */
227
+ getServiceRequest(): ModelRequest;
228
+ getServiceModelName(): string;
229
+ getServiceMongooseModel(): unknown;
230
+ /**
231
+ * VIRT-06 owning-parent internal grants (VIRT-00A D2).
232
+ *
233
+ * Dedicated subdocument routes reuse the owning parent's internally
234
+ * computed `read` grants with scoped `permissionSchema.<field>.sub` rules;
235
+ * v1 adds NO independent embedded `docPermissions` hook family. `operation`
236
+ * stays the initiating sub op (`subList`/`subRead`/`subCreate`/`subUpdate`/
237
+ * `subBulkUpdate`) for logs, while `docPermissionsAccess` is `read`.
238
+ * Never throws: hook failure yields `{}` (fail-closed downstream).
239
+ */
240
+ getSubParentGrants(parentDoc: unknown, operation: string): Promise<Record<string, unknown>>;
241
+ /**
242
+ * VIRT-06 bounded sub-row finalization (VIRT-00A D7, VIRT-03 strategy).
243
+ *
244
+ * Finalizes already-filtered (visible-only) sub rows through the shared
245
+ * boundary with scope ` [sub,'sub']`, owning-parent `read` grants, and
246
+ * scoped rules. Children (deeper nested + related via `related`) finalize
247
+ * before sub getters; only finalized children appear in getter views.
248
+ * Uses ONE bounded row map + shared `SharedHookGate` (stable order,
249
+ * peak ≤ `maxHookConcurrency`, limit-1 completes, no persistence permits
250
+ * held). Callers must filter rows (existing row/operation filters) BEFORE
251
+ * calling; hidden rows never reach getters. Never mutates inputs.
252
+ */
253
+ finalizeSubRowsWithVirtuals(args: {
254
+ sub: string;
255
+ rows: unknown[];
256
+ plan: VirtualProjectionPlan;
257
+ snapshot: FinalizeSnapshot;
258
+ parentGrants: Record<string, unknown>;
259
+ virtualAccess: VirtualOperationAccess;
260
+ outputAccess: VirtualOperationAccess;
261
+ docPermissionsAccess: VirtualOperationAccess;
262
+ operation: string;
263
+ related?: FinalizeRelatedTarget[];
264
+ }): Promise<Record<string, unknown>[]>;
265
+ /**
266
+ * VIRT-06 single sub-row finalization (same boundary/bounds as list).
267
+ * Returns `null` when input is nullish (caller maps to `null` for
268
+ * denied-post-persist single visibility, never a retryable error).
269
+ */
270
+ finalizeSingleSubRowWithVirtuals(args: {
271
+ sub: string;
272
+ row: unknown;
273
+ plan: VirtualProjectionPlan;
274
+ snapshot: FinalizeSnapshot;
275
+ parentGrants: Record<string, unknown>;
276
+ virtualAccess: VirtualOperationAccess;
277
+ outputAccess: VirtualOperationAccess;
278
+ docPermissionsAccess: VirtualOperationAccess;
279
+ operation: string;
280
+ related?: FinalizeRelatedTarget[];
281
+ }): Promise<Record<string, unknown> | null>;
282
+ /**
283
+ * VIRT-06 populate finalization for sub parents (VIRT-05 path).
284
+ *
285
+ * After Mongoose `populate` resolves on the raw parent (e.g.
286
+ * `contacts.friend` via `genSubPopulate`), finalize each actual target
287
+ * with its own plan/context before sub getters. Handles null/scalar/array
288
+ * + dotted through embedded arrays; trims even without virtuals. Bounded
289
+ * via shared gate; limit-1 completes. No-op when no entries.
290
+ */
291
+ finalizePopulateForSubParent(parentDoc: unknown, populateEntries: Populate[], parentOperation: string, hookLimit?: number, sharedGate?: SharedHookGate): Promise<void>;
292
+ /**
293
+ * Output-only write admission (VIRT-04 requirement 4). Excludes registered
294
+ * virtual names for any access (including bare `true` rules and inactive
295
+ * descriptors) and sanitizes registered embedded virtual keys inside
296
+ * whole-object/array grants. Trusted prepare/transform authority is
297
+ * preserved (this runs on client data before validation only); computed
298
+ * values are assigned only to output copies by the finalizer.
299
+ */
300
+ private admitWriteDataForVirtuals;
301
+ private filterAllowedFieldsForVirtuals;
302
+ private stripVirtualsFromFilterForSnapshot;
303
+ /**
304
+ * Preserve the `permissions` Mongoose virtual (from `permissionsPlugin`) for
305
+ * hydrated outputs. `isolateDocForOutput` uses `toObject({ virtuals: false })`
306
+ * so Mongoose getter virtuals never leak into package-virtual planning;
307
+ * `lean: true` therefore drops them (explicit parity rule). For `lean: false`
308
+ * (hydrated Mongoose documents) the historical contract exposes
309
+ * `doc.permissions` as an alias of the document-permission field. The shared
310
+ * finalizer keeps only the document-permission field, so restore the alias
311
+ * here for backward compatibility when the input was hydrated and the
312
+ * virtual existed. Lean outputs stay without the virtual. Models without
313
+ * the plugin never had the virtual and stay unchanged.
314
+ */
315
+ private preservePermissionsVirtual;
316
+ /**
317
+ * VIRT-05 populate + association helpers.
318
+ *
319
+ * - Populate targets finalize with their own plan/context before parent
320
+ * getters (target trimming even without virtuals). Handles null, scalar
321
+ * IDs when population is skipped, arrays, and dotted paths through
322
+ * embedded arrays (e.g. `contacts.friend`). One evaluation per logical
323
+ * target output, no in-place mutation of shared/cached instances (targets
324
+ * are replaced, never mutated; parents are fresh per-query docs, consistent
325
+ * with `includeDocs` in-place attachment), isolated copies per parent,
326
+ * bounded via the shared `SharedHookGate` + `mapWithConcurrencyLimit`
327
+ * (no unbounded fan-out, limit-1 completes, no persistence permits held).
328
+ * - Association transport stays service-only (symbols, outside serializers
329
+ * and finalized output input): `find` fetches `foreignField` fetch-only,
330
+ * captures immutable values before trim, and returns them aligned to rows.
331
+ */
332
+ private getAssociationFieldFromOptions;
333
+ private buildDbSelectForAssociation;
334
+ private captureAssociationValues;
335
+ private captureTargetSnapshotForPopulate;
336
+ private resolvePopulateMeta;
337
+ private isScalarPopulateValue;
338
+ private finalizeSinglePopulateTarget;
339
+ private finalizePopulatePath;
340
+ private finalizePopulateForSingleDoc;
64
341
  private beginOp;
65
342
  private completeOp;
66
343
  constructor(req: ModelRequest, modelName: string);
@@ -164,44 +441,52 @@ declare class Service<TModel = unknown> extends Base<TModel> {
164
441
  getParentDoc(id: SubdocumentId, sub: SubdocumentName, args?: SubdocumentParentArgs, options?: SubdocumentParentOptions): Promise<any>;
165
442
  }
166
443
 
167
- declare class PublicService<TModel = unknown> extends Service<TModel> {
444
+ declare class PublicService<TModel = unknown, TVirtuals extends object = Record<never, never>> extends Service<TModel, TVirtuals> {
168
445
  _list<TSelect extends Projection | undefined = undefined, TPopulate extends Populate[] | string | undefined = undefined>(filter: Filter<TModel>, args?: Omit<PublicListArgs, 'select' | 'populate'> & {
169
446
  select?: TSelect;
170
447
  populate?: TPopulate;
171
- }, options?: PublicListOptions): Promise<ListResult<SelectedPopulatedPublicOutput<TModel, TSelect, TPopulate>> | ErrorResult>;
448
+ }, options?: PublicListOptions): Promise<ListResult<SelectedPopulatedPublicOutput<TModel, TSelect, TPopulate, TVirtuals>> | ErrorResult>;
172
449
  _create<TSelect extends Projection | undefined = undefined, TPopulate extends Populate[] | string | undefined = undefined>(data: unknown, args?: Omit<PublicCreateArgs, 'select' | 'populate'> & {
173
450
  select?: TSelect;
174
451
  populate?: TPopulate;
175
- }, options?: PublicCreateOptions): Promise<ListResult<SelectedPopulatedPublicOutput<TModel, TSelect, TPopulate>> | ErrorResult>;
452
+ }, options?: PublicCreateOptions): Promise<ListResult<SelectedPopulatedPublicOutput<TModel, TSelect, TPopulate, TVirtuals>> | ErrorResult>;
176
453
  _new(args?: {
177
454
  select?: string[];
178
455
  }, options?: {
179
456
  skim?: boolean;
180
457
  includePermissions?: boolean;
181
458
  includeFieldPermissions?: boolean;
182
- }): Promise<SingleResult<PublicOutput<TModel>>>;
459
+ }): Promise<SingleResult<PublicOutput<TModel, TVirtuals>>>;
183
460
  _read<TSelect extends Projection | undefined = undefined, TPopulate extends Populate[] | string | undefined = undefined>(id: string, args?: Omit<PublicReadArgs, 'select' | 'populate'> & {
184
461
  select?: TSelect;
185
462
  populate?: TPopulate;
186
- }, options?: PublicReadOptions): Promise<SingleResult<SelectedPopulatedPublicOutput<TModel, TSelect, TPopulate>> | ErrorResult>;
463
+ }, options?: PublicReadOptions): Promise<SingleResult<SelectedPopulatedPublicOutput<TModel, TSelect, TPopulate, TVirtuals>> | ErrorResult>;
187
464
  _readFilter<TSelect extends Projection | undefined = undefined, TPopulate extends Populate[] | string | undefined = undefined>(filter: Filter<TModel>, args?: Omit<PublicReadArgs, 'select' | 'populate'> & {
188
465
  select?: TSelect;
189
466
  populate?: TPopulate;
190
467
  sort?: Sort;
191
- }, options?: PublicReadOptions): Promise<SingleResult<SelectedPopulatedPublicOutput<TModel, TSelect, TPopulate>> | ErrorResult>;
468
+ }, options?: PublicReadOptions): Promise<SingleResult<SelectedPopulatedPublicOutput<TModel, TSelect, TPopulate, TVirtuals>> | ErrorResult>;
192
469
  _update<TSelect extends Projection | undefined = undefined, TPopulate extends Populate[] | string | undefined = undefined>(id: string, data: Record<string, unknown>, args?: Omit<PublicUpdateArgs, 'select' | 'populate'> & {
193
470
  select?: TSelect;
194
471
  populate?: TPopulate;
195
- }, options?: PublicUpdateOptions): Promise<SingleResult<SelectedPopulatedPublicOutput<TModel, TSelect, TPopulate>> | ErrorResult>;
472
+ }, options?: PublicUpdateOptions): Promise<SingleResult<SelectedPopulatedPublicOutput<TModel, TSelect, TPopulate, TVirtuals>> | ErrorResult>;
196
473
  /**
197
474
  * With an _id, check update-policy existence then update: terminal denial returns
198
475
  * Forbidden, an allowed miss remains Unauthorized, and neither creates a row.
199
476
  * Without an _id, use the create path.
477
+ *
478
+ * VIRT-04 D1/D6: both branches keep `operation: 'upsert'` while accesses
479
+ * follow the taken branch (update/read/update vs create/read/create).
480
+ * Effective selection is carried via internal `overrides.effectiveSelect`
481
+ * before evaluation; presentation picks after decorate/tasks are preserved.
482
+ * Internal `updateById`/`create` are called directly (not via `_update`/
483
+ * `_create`) so the initiating operation is not overwritten with the
484
+ * branch name.
200
485
  */
201
486
  _upsert<TSelect extends Projection | undefined = undefined, TPopulate extends Populate[] | string | undefined = undefined>(data: Record<string, unknown>, args?: Omit<PublicUpsertArgs, 'select' | 'populate'> & {
202
487
  select?: TSelect;
203
488
  populate?: TPopulate;
204
- }, options?: PublicUpsertOptions): Promise<ServiceResult<SelectedPopulatedPublicOutput<TModel, TSelect, TPopulate>> | ErrorResult>;
489
+ }, options?: PublicUpsertOptions): Promise<ServiceResult<SelectedPopulatedPublicOutput<TModel, TSelect, TPopulate, TVirtuals>> | ErrorResult>;
205
490
  _delete(id: string): Promise<SingleResult<unknown> | ErrorResult>;
206
491
  _distinct(field: string, options?: DistinctArgs<TModel>): Promise<ListResult<unknown> | ErrorResult>;
207
492
  _count(filter: Filter<TModel>, access?: BaseFilterAccess): Promise<SingleResult<number> | ErrorResult>;
@@ -460,7 +745,7 @@ type AdvancedListBody = {
460
745
  includeFieldPermissions?: boolean;
461
746
  includeCount?: boolean;
462
747
  includeExtraHeaders?: boolean;
463
- populateAccess?: unknown;
748
+ populateAccess?: PopulateAccess;
464
749
  };
465
750
  };
466
751
  type CountBody = {
@@ -478,7 +763,7 @@ type AdvancedReadFilterBody = {
478
763
  includePermissions?: boolean;
479
764
  includeFieldPermissions?: boolean;
480
765
  tryList?: boolean;
481
- populateAccess?: unknown;
766
+ populateAccess?: PopulateAccess;
482
767
  };
483
768
  };
484
769
  type AdvancedReadBody = {
@@ -491,7 +776,7 @@ type AdvancedReadBody = {
491
776
  includePermissions?: boolean;
492
777
  includeFieldPermissions?: boolean;
493
778
  tryList?: boolean;
494
- populateAccess?: unknown;
779
+ populateAccess?: PopulateAccess;
495
780
  };
496
781
  };
497
782
  type AdvancedCreateBody = {
@@ -502,7 +787,7 @@ type AdvancedCreateBody = {
502
787
  options?: {
503
788
  includePermissions?: boolean;
504
789
  includeFieldPermissions?: boolean;
505
- populateAccess?: unknown;
790
+ populateAccess?: PopulateAccess;
506
791
  };
507
792
  };
508
793
  type AdvancedUpdateBody = {
@@ -514,7 +799,7 @@ type AdvancedUpdateBody = {
514
799
  returningAll?: boolean;
515
800
  includePermissions?: boolean;
516
801
  includeFieldPermissions?: boolean;
517
- populateAccess?: unknown;
802
+ populateAccess?: PopulateAccess;
518
803
  };
519
804
  };
520
805
  type AdvancedUpsertBody = {
@@ -526,7 +811,7 @@ type AdvancedUpsertBody = {
526
811
  returningAll?: boolean;
527
812
  includePermissions?: boolean;
528
813
  includeFieldPermissions?: boolean;
529
- populateAccess?: unknown;
814
+ populateAccess?: PopulateAccess;
530
815
  };
531
816
  };
532
817
  type DistinctBody = {
@@ -558,6 +843,23 @@ interface RequestComplexityOptions {
558
843
  * Not a process-wide connection limit or protection against competing edits.
559
844
  */
560
845
  maxBulkConcurrency?: number;
846
+ /**
847
+ * Defaults to 10. Per-request/per-runtime ceiling on concurrently awaited
848
+ * virtual-getter + row-finalization orchestration work (VIRT-00A D7), NOT a
849
+ * per-row multiplier and NOT leaf persistence admission. Top-level lists
850
+ * finalize rows through ONE bounded map with a shared hook gate held only
851
+ * for getter bodies (stable index-keyed order regardless of completion
852
+ * order), so peak active getter work stays at or below this limit — never
853
+ * `rows × getters`. Nested include/populate/embedded work uses bounded
854
+ * child maps with a finite bound derived from the same limit; recursion
855
+ * depth is additionally bounded by `maxCorrelatedDepth` and the correlated
856
+ * budgets. Recursive orchestration never holds leaf persistence permits
857
+ * (`RequestConcurrencyScheduler.work`, which admits only leaf
858
+ * adapter/document persistence ops) while awaiting descendants or hooks,
859
+ * so limit-1 configurations complete. Trusted direct DB/network I/O issued
860
+ * by virtual getters themselves runs outside the leaf persistence ceiling
861
+ * (no dataloader/batching in v1). Not a process-wide connection limit.
862
+ */
561
863
  maxHookConcurrency?: number;
562
864
  /** Cumulative correlated target executions per request/runtime (default 100); permit release does not refund this budget. */
563
865
  maxCorrelatedQueries?: number;
@@ -732,21 +1034,49 @@ declare class AccessRuntime {
732
1034
  getDefaultModelOption<K extends keyof ExtendedDefaultModelRouterOptions>(key: K, defaultValue?: ExtendedDefaultModelRouterOptions[K]): ExtendedDefaultModelRouterOptions<unknown>[K];
733
1035
  private createModelOptions;
734
1036
  private getOrCreateModelOptions;
735
- setModelOptions<TModel = unknown>(modelName: string, options: ModelRouterOptions<TModel>): void;
736
- setModelOption<K extends keyof ExtendedModelRouterOptions<TModel>, TModel = unknown>(modelName: string, key: K, value: ExtendedModelRouterOptions<TModel>[K]): void;
737
- getModelOptions<TModel = unknown>(modelName: string): ModelRouterOptions<TModel>;
738
- getModelOption<K extends keyof ExtendedModelRouterOptions<TModel>, TModel = unknown>(modelName: string, key: K | string, defaultValue?: ExtendedModelRouterOptions<TModel>[K]): ExtendedModelRouterOptions<TModel>[K];
739
- getExactModelOption<K extends keyof ExtendedModelRouterOptions<TModel>, TModel = unknown>(modelName: string, key: K | string): ExtendedModelRouterOptions<TModel>[K];
1037
+ setModelOptions<TModel = unknown, TVirtuals extends object = Record<never, never>>(modelName: string, options: ModelRouterOptions<TModel, TVirtuals>): void;
1038
+ setModelOption<K extends keyof ExtendedModelRouterOptions<TModel, TVirtuals>, TModel = unknown, TVirtuals extends object = Record<never, never>>(modelName: string, key: K, value: ExtendedModelRouterOptions<TModel, TVirtuals>[K]): void;
1039
+ getModelOptions<TModel = unknown, TVirtuals extends object = Record<never, never>>(modelName: string): ModelRouterOptions<TModel, TVirtuals>;
1040
+ getModelOption<K extends keyof ExtendedModelRouterOptions<TModel, TVirtuals>, TModel = unknown, TVirtuals extends object = Record<never, never>>(modelName: string, key: K | string, defaultValue?: ExtendedModelRouterOptions<TModel, TVirtuals>[K]): ExtendedModelRouterOptions<TModel, TVirtuals>[K];
1041
+ getExactModelOption<K extends keyof ExtendedModelRouterOptions<TModel, TVirtuals>, TModel = unknown, TVirtuals extends object = Record<never, never>>(modelName: string, key: K | string): ExtendedModelRouterOptions<TModel, TVirtuals>[K];
1042
+ /**
1043
+ * List registered virtual leaf names for a scope (VIRT-01).
1044
+ * Scope containers (`{ sub }`) are not leaves. Inapplicable names are
1045
+ * still listed here; applicability is decided per-access by
1046
+ * {@link resolveVirtualDescriptor}. Request-controlled include collisions
1047
+ * are validated by VIRT-05, not guessed here.
1048
+ */
1049
+ getVirtualNames(modelName: string, scopePath?: string[]): string[];
1050
+ /**
1051
+ * Whether a field is a registered virtual leaf in a scope (VIRT-01).
1052
+ * Inapplicable registered names stay virtual (never persisted), even when
1053
+ * no getter applies for the current access. Containers and persisted
1054
+ * fields return false.
1055
+ */
1056
+ isVirtualField(modelName: string, fieldName: string, scopePath?: string[]): boolean;
1057
+ /**
1058
+ * Resolve an applicable virtual descriptor with exact → `.default` → bare
1059
+ * semantics (VIRT-01, VIRT-00A D1/D5). Returns `undefined` when no getter
1060
+ * applies for `access`. Crucially, an access-record object is NEVER
1061
+ * interpreted as a getter descriptor when the access is absent: an
1062
+ * inapplicable registered name stays virtual (excluded from persisted
1063
+ * projections, write admission, and DB sort/filter/distinct).
1064
+ * Shared record-level `dependsOn` is inherited when a leaf lacks its own.
1065
+ */
1066
+ resolveVirtualDescriptor(modelName: string, virtualName: string, access: string, scopePath?: string[]): {
1067
+ get: (...args: never[]) => unknown;
1068
+ dependsOn: string[];
1069
+ } | undefined;
740
1070
  getModelNames(): string[];
741
1071
  hasModel(modelName: string): boolean;
742
1072
  getModelJsonSchema(modelName: string): Record<string, unknown>;
743
1073
  private createDataOptions;
744
1074
  private getOrCreateDataOptions;
745
1075
  setDataOptions<TData = unknown>(dataName: string, options: DataRouterOptions<TData>): void;
746
- setDataOption<K extends keyof DataRouterOptions<TData>, TData = unknown>(dataName: string, key: K, value: DataRouterOptions<TData>[K]): void;
1076
+ setDataOption<K extends keyof ExtendedDataRouterOptions<TData>, TData = unknown>(dataName: string, key: K, value: ExtendedDataRouterOptions<TData>[K]): void;
747
1077
  getDataOptions<TData = unknown>(dataName: string): DataRouterOptions<TData>;
748
1078
  getDataSnapshot<TData = unknown>(dataName: string): readonly TData[];
749
- getDataOption<K extends keyof DataRouterOptions<TData>, TData = unknown>(dataName: string, key: K | string, defaultValue?: DataRouterOptions<TData>[K]): DataRouterOptions<TData>[K];
1079
+ getDataOption<K extends keyof ExtendedDataRouterOptions<TData>, TData = unknown>(dataName: string, key: K | string, defaultValue?: ExtendedDataRouterOptions<TData>[K]): ExtendedDataRouterOptions<TData>[K];
750
1080
  getExactDataOption<K extends keyof ExtendedDataRouterOptions<TData>, TData = unknown>(dataName: string, key: K | string): ExtendedDataRouterOptions<TData>[K];
751
1081
  getDataNames(): string[];
752
1082
  ensureModelMeta(modelName: string): void;
@@ -756,22 +1086,12 @@ declare class AccessRuntime {
756
1086
  }
757
1087
  declare const defaultRuntime: AccessRuntime;
758
1088
 
759
- declare class RequestConcurrencyScheduler {
760
- readonly limit: number;
761
- private activeWork;
762
- private readonly waiters;
763
- constructor(limit: number);
764
- map<TInput, TOutput>(items: TInput[], iteratee: (item: TInput, index: number, scheduled: true) => Promise<TOutput>): Promise<TOutput[]>;
765
- run<TOutput>(iteratee: (scheduled: true) => Promise<TOutput>): Promise<TOutput>;
766
- work<TOutput>(operation: () => TOutput | PromiseLike<TOutput>): Promise<TOutput>;
767
- }
768
-
769
1089
  type CrossResourceModelOperation = 'list' | 'read' | 'count';
770
1090
  interface CorrelatedExecState {
771
1091
  totalQueries: number;
772
1092
  scheduler: RequestConcurrencyScheduler;
773
1093
  }
774
- declare class Base<TModel = unknown> {
1094
+ declare class Base<TModel = unknown, TVirtuals extends object = Record<never, never>> {
775
1095
  protected req: ModelRequest;
776
1096
  protected modelName: string;
777
1097
  protected readonly requestRuntime: AccessRuntime;
@@ -806,7 +1126,7 @@ declare class Base<TModel = unknown> {
806
1126
  getRequestComplexity(): Required<RequestComplexityOptions>;
807
1127
  protected getClientRequestErrorResult(error: unknown): ErrorResult | null;
808
1128
  protected throwClientRequestError(code: ErrorResult['code'], detail: string): never;
809
- protected getAuthorizedTargetService(modelName: string, op: CrossResourceModelOperation): Promise<PublicService<unknown>>;
1129
+ protected getAuthorizedTargetService(modelName: string, op: CrossResourceModelOperation): Promise<PublicService<unknown, Record<never, never>>>;
810
1130
  protected processInclude(include: Include | Include[]): {
811
1131
  includes: LegacyInclude[];
812
1132
  correlatedIncludes: CorrelatedInclude[];
@@ -815,6 +1135,17 @@ declare class Base<TModel = unknown> {
815
1135
  correlatedReferenceFields: string[];
816
1136
  };
817
1137
  private validateIncludeOutputPath;
1138
+ /**
1139
+ * VIRT-05 preflight: request include output paths vs registered virtual
1140
+ * paths with ancestor/descendant checks per receiving scope, before any
1141
+ * target dispatch. Registered virtual names remain virtual even when no
1142
+ * getter applies; an include that would overwrite a virtual (equal,
1143
+ * ancestor, or descendant, including bracket equivalents) fails closed
1144
+ * with `BadRequest`. Ordinary non-virtual collisions stay permitted and
1145
+ * existing permission-metadata protections above are preserved.
1146
+ */
1147
+ private collectVirtualDbPathsForPreflight;
1148
+ private validateIncludeVirtualCollision;
818
1149
  private assertIncludeOutputPaths;
819
1150
  private getForeignKeyValues;
820
1151
  private getForeignKey;
@@ -887,6 +1218,20 @@ declare class Core {
887
1218
  beforeDelete<T>(modelName: string, doc: T, context: ModelHookContext): Promise<void>;
888
1219
  afterDelete<T>(modelName: string, doc: T, context: ModelHookContext): Promise<void>;
889
1220
  genDocPermissions(modelName: string, doc: unknown, access: DocPermissionsAccess, context: ModelHookContext): Promise<{}>;
1221
+ /**
1222
+ * VIRT-03 internal grant resolution (VIRT-00A D1/D2).
1223
+ *
1224
+ * Returns full internal document grants for virtual authorization without
1225
+ * consulting response-metadata switches (`includePermissions`,
1226
+ * `includeFieldPermissions`, `skim`, `stripPermissionsField`,
1227
+ * `exposedDocPermissionKeys`, `disableFieldPermissions`): those switches
1228
+ * affect serialization at the caller boundary only, never authorization
1229
+ * inputs. When `supplied` is a plain object it is reused as-is (no second
1230
+ * hook invocation for the same output-stage access); otherwise the correct
1231
+ * `docPermissionsAccess` hook runs once. The result populates scope-aware
1232
+ * `context.docPermissions` (full grants, distinct from serialized metadata).
1233
+ */
1234
+ resolveFinalizerDocPermissions(modelName: string, doc: unknown, access: DocPermissionsAccess, context: ModelHookContext, supplied?: unknown): Promise<Record<string, unknown>>;
890
1235
  addEmptyPermissions<T>(modelName: string, doc: T): T;
891
1236
  addDocPermissions<T>(modelName: string, doc: T, access: DocPermissionsAccess, context: ModelHookContext): Promise<T>;
892
1237
  addFieldPermissions<T extends {
@@ -899,10 +1244,12 @@ declare class Core {
899
1244
  setPermissions(): Promise<void>;
900
1245
  canActivate(routeGuard: Validation): Promise<boolean>;
901
1246
  isAllowed(modelName: string, access: RouteGuardAccess | string): Promise<boolean>;
902
- getService<TModel = unknown>(modelName: string): Service<TModel>;
903
- getPublicService<TModel = unknown>(modelName: string): PublicService<TModel>;
904
- service<TModel = unknown>(modelName: string): PublicService<TModel>;
905
- svc<TModel = unknown>(modelName: string): PublicService<TModel>;
1247
+ /** Authorize route entry using explicit server-owned basic/advanced metadata. */
1248
+ isAllowedRoute(modelName: string, baseAccess: RouteGuardAccess | string, variant: RouteVariant): Promise<boolean>;
1249
+ getService<TModel = unknown, TVirtuals extends object = Record<never, never>>(modelName: string): Service<TModel, TVirtuals>;
1250
+ getPublicService<TModel = unknown, TVirtuals extends object = Record<never, never>>(modelName: string): PublicService<TModel, TVirtuals>;
1251
+ service<TModel = unknown, TVirtuals extends object = Record<never, never>>(modelName: string): PublicService<TModel, TVirtuals>;
1252
+ svc<TModel = unknown, TVirtuals extends object = Record<never, never>>(modelName: string): PublicService<TModel, TVirtuals>;
906
1253
  private getGlobalPermissions;
907
1254
  }
908
1255
 
@@ -922,6 +1269,8 @@ declare class DataCore {
922
1269
  setPermissions(): Promise<void>;
923
1270
  canActivate(routeGuard: Validation): Promise<boolean>;
924
1271
  isAllowed(dataName: string, access: RouteGuardAccess | string): Promise<boolean>;
1272
+ /** Authorize route entry using explicit server-owned basic/advanced metadata. */
1273
+ isAllowedRoute(dataName: string, baseAccess: RouteGuardAccess | string, variant: RouteVariant): Promise<boolean>;
925
1274
  getService<TData = unknown>(dataName: string): DataService<TData>;
926
1275
  service<TData = unknown>(dataName: string): DataService<TData>;
927
1276
  svc<TData = unknown>(dataName: string): DataService<TData>;
@@ -982,6 +1331,24 @@ interface PublicCreateArgs {
982
1331
  tasks?: Task | Task[];
983
1332
  }
984
1333
  interface CreateArgs extends Omit<PublicCreateArgs, 'select' | 'tasks'> {
1334
+ overrides?: {
1335
+ populate?: Populate[] | string;
1336
+ /**
1337
+ * VIRT-04 internal plan transport (VIRT-00A D6).
1338
+ * Effective mutation output selection carried into internal finalization
1339
+ * before virtual evaluation. Internal create args omit `select` publicly;
1340
+ * this trusted transport supplies the effective selection (explicit
1341
+ * `select` or `undefined` for all) without changing public presentation
1342
+ * (public `_create` still picks after decorate/tasks).
1343
+ */
1344
+ effectiveSelect?: Projection;
1345
+ /**
1346
+ * VIRT-04 initiating operation (VIRT-00A D1). Upsert branches keep
1347
+ * `operation: 'upsert'` while virtual/output/doc accesses follow the
1348
+ * taken branch. Defaults to `'create'` when absent.
1349
+ */
1350
+ operation?: string;
1351
+ };
985
1352
  }
986
1353
  interface PublicCreateOptions {
987
1354
  skim?: boolean;
@@ -1064,13 +1431,34 @@ interface UpdateOneArgs<T = unknown> extends Omit<PublicUpdateArgs, 'select' | '
1064
1431
  /** Trusted replacement: nullish generates row policy; false returns Forbidden without filter generation or persistence. */
1065
1432
  filter?: Filter<T>;
1066
1433
  populate?: Populate[] | string;
1434
+ /**
1435
+ * VIRT-04 internal plan transport (VIRT-00A D6).
1436
+ * Effective mutation output selection carried into internal finalization
1437
+ * before virtual evaluation (explicit `select`, `returningAll: false`
1438
+ * implicit `Object.keys(data)+_id`, or `undefined` for all). Public
1439
+ * `_update` still enforces presentation pick after decorate/tasks.
1440
+ */
1441
+ effectiveSelect?: Projection;
1442
+ /**
1443
+ * VIRT-04 initiating operation (VIRT-00A D1). Upsert branches keep
1444
+ * `operation: 'upsert'` while accesses follow the taken branch.
1445
+ * Defaults to `'update'` when absent.
1446
+ */
1447
+ operation?: string;
1067
1448
  };
1068
1449
  }
1069
- interface UpdateByIdArgs<T = unknown> extends Omit<UpdateOneArgs<T>, 'overrides'> {
1450
+ interface UpdateByIdArgs<T = unknown> extends Omit<PublicUpdateArgs, 'select' | 'tasks'> {
1070
1451
  overrides?: {
1071
1452
  populate?: Populate[] | string;
1072
1453
  /** Nullish resolves the identifier; false denies without ID/filter generation or persistence. Objects still receive row policy. */
1073
1454
  idFilter?: Filter<T>;
1455
+ /**
1456
+ * VIRT-04 internal plan transport (VIRT-00A D6): effective output
1457
+ * selection for virtual planning before decorate/tasks.
1458
+ */
1459
+ effectiveSelect?: Projection;
1460
+ /** VIRT-04 initiating operation (VIRT-00A D1); defaults to `'update'`. */
1461
+ operation?: string;
1074
1462
  };
1075
1463
  }
1076
1464
  interface UpsertArgs<T = unknown> extends UpdateOneArgs<T> {
@@ -1137,7 +1525,13 @@ type WithDefaultId<T> = T extends object ? T & {
1137
1525
  } : {
1138
1526
  _id?: unknown;
1139
1527
  };
1140
- type PublicOutput<T> = T extends object ? T & Record<string, unknown> : Record<string, unknown>;
1528
+ /**
1529
+ * Public output with optional computed virtual shape (VIRT-01, VIRT-00A D3).
1530
+ * Virtual names are NEVER added to persisted `Filter`, sort/distinct inputs,
1531
+ * or client write types — only selected output shape gains optional virtuals.
1532
+ */
1533
+ type WithVirtuals<TModel, TVirtuals extends object = Record<never, never>> = TModel & Partial<TVirtuals>;
1534
+ type PublicOutput<T, TVirtuals extends object = Record<never, never>> = WithVirtuals<T, TVirtuals> extends object ? WithVirtuals<T, TVirtuals> & Record<string, unknown> : Record<string, unknown>;
1141
1535
  type ModelDocument<TModel = unknown> = mongoose.Document & TModel;
1142
1536
  type TrimLeft<S extends string> = S extends ` ${infer Rest}` ? TrimLeft<Rest> : S;
1143
1537
  type TrimRight<S extends string> = S extends `${infer Rest} ` ? TrimRight<Rest> : S;
@@ -1155,7 +1549,12 @@ type DeepPickSingle<T, TPath extends string> = TPath extends `${infer Head}.${in
1155
1549
  [K in Head]: WithDefaultId<T>[K] extends readonly (infer U)[] ? Array<Simplify<DeepPickSingle<U, Tail>>> : WithDefaultId<T>[K] extends object ? Simplify<DeepPickSingle<WithDefaultId<T>[K], Tail>> : WithDefaultId<T>[K];
1156
1550
  } : {} : {} : TPath extends keyof WithDefaultId<T> ? Pick<WithDefaultId<T>, TPath> : {};
1157
1551
  type DeepPick<T, TPaths extends string> = Simplify<UnionToIntersection<DeepPickSingle<T, TPaths>>>;
1158
- type SelectedPublicOutput<T, TProjection> = string extends PositiveProjectionPaths<TProjection> ? PublicOutput<T> : [PositiveProjectionPaths<TProjection>] extends [never] ? PublicOutput<T> : DeepPick<T, Extract<PositiveProjectionPaths<TProjection>, string>>;
1552
+ /**
1553
+ * Literal-select output with optional virtuals (VIRT-01, VIRT-00A D3).
1554
+ * Virtual outputs stay optional (`Partial`) because authorization, absent
1555
+ * dependencies, `undefined`, and fail-closed errors can omit them.
1556
+ */
1557
+ type SelectedPublicOutput<T, TProjection, TVirtuals extends object = Record<never, never>> = string extends PositiveProjectionPaths<TProjection> ? PublicOutput<T, TVirtuals> : [PositiveProjectionPaths<TProjection>] extends [never] ? PublicOutput<T, TVirtuals> : DeepPick<WithVirtuals<T, TVirtuals>, Extract<PositiveProjectionPaths<TProjection>, string>>;
1159
1558
  type PopulateInput = Populate | string;
1160
1559
  type PopulateInputs<TPopulate> = TPopulate extends readonly (infer U)[] ? U : TPopulate;
1161
1560
  type PopulatePath<TPopulateEntry> = TPopulateEntry extends string ? TPopulateEntry : TPopulateEntry extends {
@@ -1172,7 +1571,7 @@ type PopulateEntryShape<T, TPath extends string, TValue> = TPath extends `${infe
1172
1571
  }, TPath> : {};
1173
1572
  type PopulateOutputShape<T, TPopulateEntry> = PopulatePath<TPopulateEntry> extends infer TPath extends string ? PopulateEntryShape<T, TPath, PopulateLeafValue<PathValue<WithDefaultId<T>, TPath>, TPopulateEntry>> : {};
1174
1573
  type PopulateOutput<T, TPopulate> = string extends PopulatePath<PopulateInputs<TPopulate>> ? {} : [PopulatePath<PopulateInputs<TPopulate>>] extends [never] ? {} : Simplify<UnionToIntersection<PopulateOutputShape<T, Extract<PopulateInputs<TPopulate>, PopulateInput>>>>;
1175
- type SelectedPopulatedPublicOutput<T, TProjection, TPopulate> = Simplify<SelectedPublicOutput<T, TProjection> & PopulateOutput<T, TPopulate>>;
1574
+ type SelectedPopulatedPublicOutput<T, TProjection, TPopulate, TVirtuals extends object = Record<never, never>> = Simplify<SelectedPublicOutput<T, TProjection, TVirtuals> & PopulateOutput<T, TPopulate>>;
1176
1575
  type Unwrap<T> = NonNullable<Descend<T>>;
1177
1576
  type DeepFieldPath<T, Depth extends number = 4> = [Depth] extends [never] ? never : Unwrap<T> extends NonTraversable ? never : {
1178
1577
  [K in Extract<keyof Unwrap<T>, string>]: K | (NonNullable<Unwrap<T>[K]> extends NonTraversable ? never : NonNullable<Unwrap<T>[K]> extends readonly (infer U)[] ? Unwrap<U> extends NonTraversable ? never : `${K}.${DeepFieldPath<U, PrevDepth[Depth]>}` : NonNullable<Unwrap<T>[K]> extends object ? `${K}.${DeepFieldPath<NonNullable<Unwrap<T>[K]>, PrevDepth[Depth]>}` : never);
@@ -1303,7 +1702,16 @@ interface SubPopulate {
1303
1702
  type GuardHook<TRequest extends AccessRouterRequest = AccessRouterRequest> = (this: TRequest, permissions: AccessRouterPermissions) => boolean | Promise<boolean>;
1304
1703
  type Validation = boolean | string | string[] | GuardHook;
1305
1704
  type SelectAccess = 'list' | 'create' | 'read' | 'update' | string;
1306
- type RouteGuardAccess = 'new' | 'list' | 'read' | 'update' | 'upsert' | 'delete' | 'create' | 'distinct' | 'count' | 'subs' | string;
1705
+ /** Base operations with both basic and advanced generated model routes. */
1706
+ type PairedRouteAccess = 'list' | 'read' | 'create' | 'update' | 'upsert' | 'count' | 'distinct';
1707
+ /** Existing base route guards, including unpaired operations and subdocument access. */
1708
+ type RouteBaseAccess = PairedRouteAccess | 'new' | 'delete' | 'subs';
1709
+ /** Server-owned transport variant for a paired generated route. */
1710
+ type RouteVariant = 'basic' | 'advanced';
1711
+ /** The fourteen route-only keys, or the two keys for a specified paired base operation. */
1712
+ type RouteVariantAccess<TAccess extends PairedRouteAccess = PairedRouteAccess> = `${RouteVariant}${Capitalize<TAccess>}`;
1713
+ /** Known route guards with the existing trusted custom access-string compatibility. */
1714
+ type RouteGuardAccess = RouteBaseAccess | RouteVariantAccess | (string & {});
1307
1715
  type DocPermissionsAccess = 'list' | 'create' | 'read' | 'update' | string;
1308
1716
  type BaseFilterAccess = 'list' | 'read' | 'update' | 'delete' | string;
1309
1717
  type DecorateAccess = 'list' | 'create' | 'read' | 'update' | string;
@@ -1686,6 +2094,161 @@ type ModelHook<TValue = unknown> = HookChain<TValue, ModelHookContext, ModelRequ
1686
2094
  type ModelListHook<TValue = unknown> = HookChain<TValue[], ModelHookContext, ModelRequest>;
1687
2095
  type DataHook<TValue = unknown> = HookChain<TValue, DataHookContext, DataRequest>;
1688
2096
  type DataListHook<TValue = unknown> = HookChain<TValue[], DataHookContext, DataRequest>;
2097
+ /**
2098
+ * Supported per-access virtual getter keys (VIRT-00A D1/D5).
2099
+ * `default` is a fallback access; `delete`/`distinct`/`count` are NOT
2100
+ * virtual accesses and must be rejected at configuration time.
2101
+ */
2102
+ type ModelVirtualAccess = 'list' | 'create' | 'read' | 'update';
2103
+ type ModelVirtualRecordAccess = 'default' | ModelVirtualAccess;
2104
+ /**
2105
+ * Scope-aware virtual getter context (VIRT-00A D2, frozen).
2106
+ * Extends {@link ModelHookContext} with the receiving model, definition/
2107
+ * permission `scopePath` (e.g. `[]` at root, `['contacts','sub']` for
2108
+ * `contacts.sub.displayName`), and the three effective accesses. The
2109
+ * initiating operation stays in `context.operation` and is never
2110
+ * overwritten with fallback/effective visibility.
2111
+ *
2112
+ * Embedded outputs reuse the owning parent's internally computed grants with
2113
+ * the scope's `permissionSchema.<field>.sub` rules; dedicated subdocument
2114
+ * routes resolve grants from the raw parent lookup. Related (populate/
2115
+ * include) targets finalize with the target model's own definitions,
2116
+ * permissions, selection, and effective access before parent getters run.
2117
+ */
2118
+ interface ModelVirtualContext extends ModelHookContext {
2119
+ receivingModelName: string;
2120
+ scopePath: string[];
2121
+ virtualAccess: ModelVirtualAccess;
2122
+ outputAccess: ModelVirtualAccess;
2123
+ docPermissionsAccess: ModelVirtualAccess;
2124
+ }
2125
+ type IsUnknownVirtual<T> = unknown extends T ? ([keyof T] extends [never] ? true : false) : false;
2126
+ /**
2127
+ * Typed isolated persisted view for a virtual getter scope (VIRT-00A D3/D4).
2128
+ * Known models use a partial read-only persisted view (never
2129
+ * `Record<string, unknown>`); unknown/untyped models keep an explicit
2130
+ * loose fallback.
2131
+ */
2132
+ type ModelVirtualDoc<TModel = unknown> = IsUnknownVirtual<TModel> extends true ? Readonly<Partial<Record<string, unknown>>> : [Extract<keyof TModel, string>] extends [never] ? Readonly<Partial<Record<string, unknown>>> : Readonly<Partial<TModel>>;
2133
+ /**
2134
+ * Virtual getter signature (VIRT-00A D3/D4, frozen).
2135
+ * Receives an isolated plain-object snapshot of persisted dependencies +
2136
+ * already-finalized related/embedded outputs for its scope. Sibling virtual
2137
+ * values are not visible. Mutating the input cannot change response data or
2138
+ * lifecycle snapshots. Only the returned field value is committed;
2139
+ * `undefined` omits the field; throws omit fail-closed. `this` is the
2140
+ * model request.
2141
+ */
2142
+ type ModelVirtualGetter<TModel = unknown, TValue = unknown> = (this: ModelRequest, doc: ModelVirtualDoc<TModel>, permissions: AccessRouterPermissions, context: ModelVirtualContext) => MaybePromise<TValue | undefined>;
2143
+ /**
2144
+ * Leaf virtual descriptor (VIRT-01): `{ get, dependsOn }`.
2145
+ * `dependsOn` lists top-level persisted field names relative to the
2146
+ * definition's scope (e.g. `['displayName']` inside `contacts.sub`, never
2147
+ * `['contacts.sub.displayName']`). Dotted paths and virtual-to-virtual
2148
+ * dependencies are rejected at configuration time.
2149
+ *
2150
+ * The getter runs after `toObject()`/lean normalization and after internal
2151
+ * document-permissions computation, with global permissions available and
2152
+ * before `decorate`/`decorateAll`/tasks. Only the returned field value is
2153
+ * committed via document-aware helpers: `undefined` (or a throw) omits the
2154
+ * field fail-closed and computed values never persist. Getters see an
2155
+ * isolated plain-object view for both lean and hydrated results; mutating the
2156
+ * input cannot change response data or lifecycle snapshots.
2157
+ *
2158
+ * @example
2159
+ * ```ts
2160
+ * import type { ModelVirtualDescriptor } from '@web-ts-toolkit/access-router';
2161
+ * interface User { name: string; address: string; }
2162
+ * const descriptor: ModelVirtualDescriptor<User, string> = {
2163
+ * dependsOn: ['address'],
2164
+ * get: async (doc) => (doc.address === undefined ? undefined : `addr:${doc.address}`),
2165
+ * };
2166
+ * ```
2167
+ */
2168
+ interface ModelVirtualDescriptor<TModel = unknown, TValue = unknown> {
2169
+ get: ModelVirtualGetter<TModel, TValue>;
2170
+ dependsOn?: Array<AccessRouterFieldKey<TModel>>;
2171
+ }
2172
+ /**
2173
+ * Per-access virtual record (VIRT-01): `default` + `list`/`create`/`read`/
2174
+ * `update` (NOT `delete`/`distinct`/`count`). Each present access holds a
2175
+ * leaf descriptor or bare getter shorthand. An optional shared `dependsOn`
2176
+ * supports the documented `{ dependsOn, read: getter }` shape; a leaf's own
2177
+ * `dependsOn` wins over the shared list during resolution.
2178
+ */
2179
+ type ModelVirtualAccessRecord<TModel = unknown, TValue = unknown> = {
2180
+ default?: ModelVirtualDescriptor<TModel, TValue> | ModelVirtualGetter<TModel, TValue>;
2181
+ list?: ModelVirtualDescriptor<TModel, TValue> | ModelVirtualGetter<TModel, TValue>;
2182
+ create?: ModelVirtualDescriptor<TModel, TValue> | ModelVirtualGetter<TModel, TValue>;
2183
+ read?: ModelVirtualDescriptor<TModel, TValue> | ModelVirtualGetter<TModel, TValue>;
2184
+ update?: ModelVirtualDescriptor<TModel, TValue> | ModelVirtualGetter<TModel, TValue>;
2185
+ dependsOn?: Array<AccessRouterFieldKey<TModel>>;
2186
+ };
2187
+ /** Single virtual entry: bare getter, leaf descriptor, or per-access record. */
2188
+ type ModelVirtualLeaf<TModel = unknown, TValue = unknown> = ModelVirtualGetter<TModel, TValue> | ModelVirtualDescriptor<TModel, TValue> | ModelVirtualAccessRecord<TModel, TValue>;
2189
+ type UnwrapVirtualArray<T> = T extends readonly (infer U)[] ? U : T;
2190
+ /**
2191
+ * Persisted sub-scope model for an embedded container field (VIRT-01).
2192
+ * Unwraps arrays/nullable wrappers to the element/object type; unknown
2193
+ * fields fall back to a loose record.
2194
+ */
2195
+ type VirtualSubModel<TModel, TField extends string> = TField extends keyof TModel ? UnwrapVirtualArray<NonNullable<TModel[TField]>> : Record<string, unknown>;
2196
+ type LooseVirtualLeaf = ModelVirtualGetter<Record<string, unknown>, unknown> | ModelVirtualDescriptor<Record<string, unknown>, unknown> | ModelVirtualAccessRecord<Record<string, unknown>, unknown>;
2197
+ type LooseVirtualSubEntry = LooseVirtualLeaf | {
2198
+ sub?: Record<string, LooseVirtualLeaf | {
2199
+ sub?: Record<string, LooseVirtualLeaf>;
2200
+ }>;
2201
+ };
2202
+ /**
2203
+ * Virtual definitions for a model scope (VIRT-01).
2204
+ * Root leaves come from `TVirtuals` (strict: unknown persisted getter
2205
+ * fields are type errors). Embedded containers mirror
2206
+ * `permissionSchema.<field>.sub`: any persisted field may host a `sub`
2207
+ * record whose leaves use the container's persisted sub-model view.
2208
+ * Untyped models keep an explicit loose record fallback.
2209
+ *
2210
+ * Selected virtual outputs are optional (`Partial`): authorization, absent
2211
+ * dependencies, `undefined`, and fail-closed errors can omit them. Virtual
2212
+ * names are never added to persisted `Filter`, sort/distinct inputs, or
2213
+ * client write types.
2214
+ *
2215
+ * @example End-to-end typed virtual (persisted `User` has no `fullAddress`):
2216
+ * ```ts
2217
+ * import acl from '@web-ts-toolkit/access-router';
2218
+ * import type { SelectedPublicOutput } from '@web-ts-toolkit/access-router/advanced';
2219
+ * interface User { name: string; address: string; }
2220
+ * interface UserVirtuals { fullAddress: string; }
2221
+ * const router = acl.createRouter<User, UserVirtuals>('User', {
2222
+ * permissionSchema: {
2223
+ * name: { read: true },
2224
+ * address: { read: 'canViewAddress' },
2225
+ * fullAddress: { read: 'canViewAddress' },
2226
+ * },
2227
+ * virtuals: {
2228
+ * fullAddress: {
2229
+ * dependsOn: ['address'],
2230
+ * read: async function (doc) {
2231
+ * if (doc.address === undefined) return undefined;
2232
+ * return `addr:${doc.address}`;
2233
+ * },
2234
+ * },
2235
+ * },
2236
+ * });
2237
+ * type Out = SelectedPublicOutput<User, ['name', 'fullAddress'], UserVirtuals>;
2238
+ * const out: Out = { name: 'Ada', fullAddress: 'addr:x' };
2239
+ * const omitted: Out = { name: 'Ada' }; // virtual is optional when denied/missing
2240
+ * void [router, out, omitted];
2241
+ * ```
2242
+ */
2243
+ type ModelVirtuals<TModel = unknown, TVirtuals extends object = Record<never, never>> = IsUnknownVirtual<TModel> extends true ? Record<string, LooseVirtualSubEntry> : {
2244
+ [K in Extract<keyof TVirtuals, string>]?: ModelVirtualLeaf<TModel, TVirtuals[K]>;
2245
+ } & {
2246
+ [K in Extract<keyof TModel, string>]?: {
2247
+ sub?: Record<string, ModelVirtualLeaf<VirtualSubModel<TModel, K>, unknown> | {
2248
+ sub?: Record<string, LooseVirtualLeaf>;
2249
+ }>;
2250
+ };
2251
+ };
1689
2252
 
1690
2253
  interface DefaultFindOneArgs<TModel = unknown> extends Omit<FindOneArgs<TModel>, 'overrides'> {
1691
2254
  }
@@ -1698,7 +2261,33 @@ type NestedRequestSchema = {
1698
2261
  default?: RequestSchema;
1699
2262
  data?: RequestSchema;
1700
2263
  };
1701
- type SubRouteGuardOptions = Record<string, Validation | Record<string, Validation>>;
2264
+ /**
2265
+ * One field's subdocument route guards: exact variant → field base → field scalar
2266
+ * or closed object. Only an absent field rule falls back to top-level variant/base
2267
+ * guards. A defined object with no applicable key denies; nested `.default` is not
2268
+ * a fallback. Dynamic legacy operation names remain supported.
2269
+ */
2270
+ interface SubOperationAccess {
2271
+ /** Base list guard for this field; fallback for both subdocument list variants. */
2272
+ list?: Validation;
2273
+ /** Base read guard for this field; fallback for both subdocument read variants. */
2274
+ read?: Validation;
2275
+ create?: Validation;
2276
+ update?: Validation;
2277
+ delete?: Validation;
2278
+ /** GET /:id/<field>; undefined inherits this field's `list` rule. */
2279
+ basicList?: Validation;
2280
+ /** POST /:id/<field>/__query; undefined inherits this field's `list` rule. */
2281
+ advancedList?: Validation;
2282
+ /** GET /:id/<field>/:subId (and HEAD); undefined inherits this field's `read` rule. */
2283
+ basicRead?: Validation;
2284
+ /** POST /:id/<field>/:subId/__query; undefined inherits this field's `read` rule. */
2285
+ advancedRead?: Validation;
2286
+ [access: string]: Validation | undefined;
2287
+ }
2288
+ /** Per-field subdocument route objects or scalar guards; field names remain dynamic. */
2289
+ type SubRouteGuardOptions = Record<string, Validation | SubOperationAccess>;
2290
+ type SubFieldPermissionOptions = Record<string, Validation | Record<string, Validation>>;
1702
2291
  interface RequestSchemas {
1703
2292
  create?: RequestSchema;
1704
2293
  update?: RequestSchema;
@@ -1767,13 +2356,15 @@ interface GlobalOptions {
1767
2356
  }
1768
2357
  interface RootRouterOptions {
1769
2358
  basePath: string;
2359
+ /** Guard for the batch endpoint. Entries use target base operations, never basic/advanced variant names. */
1770
2360
  operationAccess?: Validation;
1771
2361
  maxBatchEntries?: number;
1772
2362
  maxOrderGroups?: number;
1773
2363
  /** Whole root operations per order group; separate from requestComplexity.maxBulkConcurrency persistence admission. */
1774
2364
  maxConcurrentOperations?: number;
1775
2365
  }
1776
- interface OperationAccess {
2366
+ /** Base-operation field grants used by PermissionSchema. Basic/advanced route keys do not grant fields. */
2367
+ interface FieldOperationAccess {
1777
2368
  new?: Validation;
1778
2369
  list?: Validation;
1779
2370
  create?: Validation;
@@ -1783,9 +2374,75 @@ interface OperationAccess {
1783
2374
  delete?: Validation;
1784
2375
  distinct?: Validation;
1785
2376
  count?: Validation;
2377
+ subs?: Validation | SubFieldPermissionOptions;
2378
+ }
2379
+ /**
2380
+ * Live generated-route guards, separate from PermissionSchema field grants.
2381
+ * An exact basic/advanced rule replaces its base guard; only undefined inherits
2382
+ * the base operation's exact → `.default` → scalar-shorthand resolution. Selected
2383
+ * false/empty-array/false-returning guards are terminal, not ANDed with the base.
2384
+ * Model exact lookup includes runtime-default variants; data has no model defaults.
2385
+ * Denial is HTTP 401; routes remain registered in getEndpoints() and OpenAPI.
2386
+ * Field/row/hook, root-entry, related-target and mutation-response policies keep
2387
+ * their existing base operations. Paths below use the default route segments.
2388
+ */
2389
+ interface OperationAccess extends Omit<FieldOperationAccess, 'subs'> {
2390
+ /** GET /new guard; unpaired, with no basic/advanced override. */
2391
+ new?: Validation;
2392
+ /** Base list guard and fallback for both list routes; also governs model read-to-list retries. */
2393
+ list?: Validation;
2394
+ /** Base create guard and fallback for both model create routes. */
2395
+ create?: Validation;
2396
+ /** Base read guard and fallback for both read variants; advancedRead covers both POST read routes. */
2397
+ read?: Validation;
2398
+ /** Base update guard and fallback for both model update routes. */
2399
+ update?: Validation;
2400
+ /** Base upsert guard and fallback for both upserts; create/update route variants do not gate its branches. */
2401
+ upsert?: Validation;
2402
+ /** DELETE /:id guard; unpaired, with no basic/advanced override. */
2403
+ delete?: Validation;
2404
+ /** Base distinct guard and fallback for GET and POST /distinct/:field. */
2405
+ distinct?: Validation;
2406
+ /** Base count guard and fallback for GET and POST /count. */
2407
+ count?: Validation;
2408
+ /** Fallback for a base operation without an exact rule; explicit base/variant guards take precedence. */
2409
+ default?: Validation;
2410
+ /** GET / (and HEAD) override for model/data list; undefined inherits `list`. */
2411
+ basicList?: Validation;
2412
+ /** POST /__query override for model/data list; undefined inherits `list`. */
2413
+ advancedList?: Validation;
2414
+ /** GET /:id (and HEAD) override for model/data read; undefined inherits `read`. */
2415
+ basicRead?: Validation;
2416
+ /** POST /__query/:id and /__query/__filter override for model/data read; undefined inherits `read`. */
2417
+ advancedRead?: Validation;
2418
+ /** POST / model create override; undefined inherits `create`. */
2419
+ basicCreate?: Validation;
2420
+ /** POST /__mutation model create override; undefined inherits `create`. */
2421
+ advancedCreate?: Validation;
2422
+ /** PATCH /:id model update override; undefined inherits `update`. */
2423
+ basicUpdate?: Validation;
2424
+ /** PATCH /__mutation/:id model update override; undefined inherits `update`. */
2425
+ advancedUpdate?: Validation;
2426
+ /** PUT / model upsert override; undefined inherits `upsert`. */
2427
+ basicUpsert?: Validation;
2428
+ /** PUT /__mutation model upsert override; undefined inherits `upsert`. */
2429
+ advancedUpsert?: Validation;
2430
+ /** GET /count model count override; undefined inherits `count`. */
2431
+ basicCount?: Validation;
2432
+ /** POST /count model filtered-count override; undefined inherits `count`. */
2433
+ advancedCount?: Validation;
2434
+ /** GET /distinct/:field model distinct override; undefined inherits `distinct`. */
2435
+ basicDistinct?: Validation;
2436
+ /** POST /distinct/:field model filtered-distinct override; undefined inherits `distinct`. */
2437
+ advancedDistinct?: Validation;
2438
+ /**
2439
+ * Per-field scalar or closed-object guards, with list/read variants. Legacy scalar
2440
+ * `subs` is accepted but is not an umbrella guard for absent fields; they use top-level rules.
2441
+ */
1786
2442
  subs?: Validation | SubRouteGuardOptions;
1787
2443
  }
1788
- type PermissionRule = Validation | OperationAccess;
2444
+ type PermissionRule = Validation | FieldOperationAccess;
2445
+ /** Field grants by base operation. Put basic/advanced route guards in operationAccess, not field-rule objects. */
1789
2446
  type PermissionSchema<TField extends string = string> = Partial<Record<TField, PermissionRule>>;
1790
2447
  interface DocPermissions {
1791
2448
  list?: ModelDocPermissionsHook;
@@ -1803,6 +2460,14 @@ interface DefaultModelRouterOptions<TModel = unknown> {
1803
2460
  parentPath?: string;
1804
2461
  queryRouteSegment?: string;
1805
2462
  mutationRouteSegment?: string;
2463
+ /**
2464
+ * Live model route guards: scalar Validation shorthand or base/variant rules.
2465
+ * `{ list: true, read: true, basicRead: false }` denies only GET /:id (and HEAD)
2466
+ * among top-level routes, retaining both advanced read POSTs subject to ordinary
2467
+ * field/row policy. Subdocument reads follow their field-rule precedence.
2468
+ * Exact runtime-default variants precede base fallback; option objects replace
2469
+ * top-level keys shallowly, rather than recursively merging operationAccess.
2470
+ */
1806
2471
  operationAccess?: Validation | OperationAccess;
1807
2472
  modelPermissionPrefix?: string;
1808
2473
  /**
@@ -1861,17 +2526,80 @@ interface ExtendedDefaultModelRouterOptions<TModel = unknown> extends DefaultMod
1861
2526
  'operationAccess.create'?: Validation;
1862
2527
  'operationAccess.distinct'?: Validation;
1863
2528
  'operationAccess.count'?: Validation;
1864
- 'operationAccess.subs'?: SubRouteGuardOptions;
1865
- }
1866
- interface ModelRouterOptions<TModel = unknown> extends DefaultModelRouterOptions<TModel> {
2529
+ 'operationAccess.basicList'?: Validation;
2530
+ 'operationAccess.advancedList'?: Validation;
2531
+ 'operationAccess.basicRead'?: Validation;
2532
+ 'operationAccess.advancedRead'?: Validation;
2533
+ 'operationAccess.basicCreate'?: Validation;
2534
+ 'operationAccess.advancedCreate'?: Validation;
2535
+ 'operationAccess.basicUpdate'?: Validation;
2536
+ 'operationAccess.advancedUpdate'?: Validation;
2537
+ 'operationAccess.basicUpsert'?: Validation;
2538
+ 'operationAccess.advancedUpsert'?: Validation;
2539
+ 'operationAccess.basicCount'?: Validation;
2540
+ 'operationAccess.advancedCount'?: Validation;
2541
+ 'operationAccess.basicDistinct'?: Validation;
2542
+ 'operationAccess.advancedDistinct'?: Validation;
2543
+ 'operationAccess.subs'?: Validation | SubRouteGuardOptions;
2544
+ }
2545
+ interface ModelRouterOptions<TModel = unknown, TVirtuals extends object = Record<never, never>> extends DefaultModelRouterOptions<TModel> {
1867
2546
  modelName?: string;
1868
2547
  basePath?: string;
1869
2548
  /**
1870
2549
  * Update grants define assignment boundaries: authorized leaves preserve omitted
1871
2550
  * siblings; a whole-object/array grant permits replacement (no generic deep merge).
1872
2551
  * Send nested JSON; literal dotted client keys are not update operators.
2552
+ * Basic/advanced keys are route guards in operationAccess, not field grants here.
2553
+ */
2554
+ permissionSchema?: PermissionSchema<AccessRouterFieldKey<TModel> | Extract<keyof TVirtuals, string>>;
2555
+ /**
2556
+ * Package-level computed fields ("virtuals", VIRT-01).
2557
+ * Typed getter/descriptor/output generics per VIRT-00A D3: typed getters
2558
+ * use a partial read-only persisted view for their scope, never
2559
+ * `Record<string, unknown>` for a known model; untyped consumers keep an
2560
+ * explicit loose fallback. Per-access records use `default` +
2561
+ * `list`/`create`/`read`/`update` (NOT `delete`/`distinct`/`count`) and
2562
+ * recursive embedded `sub` scopes mirroring
2563
+ * `permissionSchema.<field>.sub`. Inapplicable registered names stay
2564
+ * virtual (never persisted). Mutable post-construction (not a build-time
2565
+ * key); replacement swaps the frozen snapshot so in-flight planning keeps
2566
+ * a coherent version.
2567
+ *
2568
+ * Getters run after `toObject()`/lean normalization (both lean and
2569
+ * hydrated results compute), after internal document-permissions
2570
+ * computation, and before `decorate`/`decorateAll`/tasks and final
2571
+ * trimming. `dependsOn` fields are fetched internally even when the caller
2572
+ * may not receive them, then stripped unless independently selected and
2573
+ * authorized. Virtuals are output-only: registered names (including
2574
+ * embedded `sub` leaves) are excluded from persisted projections, client
2575
+ * write admission, and database sort/filter/distinct handling.
2576
+ *
2577
+ * @example Shortest typed happy-path (persisted model lacks `fullAddress`):
2578
+ * ```ts
2579
+ * import acl from '@web-ts-toolkit/access-router';
2580
+ * interface User { name: string; address: string; }
2581
+ * interface UserVirtuals { fullAddress: string; }
2582
+ * const router = acl.createRouter<User, UserVirtuals>('User', {
2583
+ * permissionSchema: {
2584
+ * name: { read: true },
2585
+ * address: { read: 'canViewAddress' },
2586
+ * fullAddress: { read: 'canViewAddress' },
2587
+ * },
2588
+ * virtuals: {
2589
+ * fullAddress: {
2590
+ * dependsOn: ['address'],
2591
+ * read: async function (doc) {
2592
+ * if (doc.address === undefined) return undefined;
2593
+ * return `addr:${doc.address}`;
2594
+ * },
2595
+ * },
2596
+ * },
2597
+ * });
2598
+ * // Selected output: { name: string; fullAddress?: string } — virtual is optional.
2599
+ * void router;
2600
+ * ```
1873
2601
  */
1874
- permissionSchema?: PermissionSchema<AccessRouterFieldKey<TModel>>;
2602
+ virtuals?: ModelVirtuals<TModel, TVirtuals>;
1875
2603
  alwaysSelectFields?: string[];
1876
2604
  docPermissions?: DocPermissions | ModelDocPermissionsHook;
1877
2605
  /**
@@ -1923,9 +2651,16 @@ interface DataRouterOptions<TData = unknown> {
1923
2651
  resolveIdFilter?: DataIdentifierHook<TData>;
1924
2652
  parentPath?: string;
1925
2653
  queryRouteSegment?: string;
2654
+ /**
2655
+ * Live data-route guards: list/read plus basicList/advancedList/basicRead/advancedRead.
2656
+ * Only undefined variants inherit their base; false denies with HTTP 401 while
2657
+ * routes remain registered. Data never inherits model defaults. The broader
2658
+ * legacy operation-rule object and scalar Validation shorthand remain accepted.
2659
+ */
1926
2660
  operationAccess?: Validation | OperationAccess;
1927
2661
  dataName?: string;
1928
2662
  basePath?: string;
2663
+ /** Base-operation field grants, independent of operationAccess's basic/advanced route guards. */
1929
2664
  permissionSchema?: PermissionSchema<AccessRouterFieldKey<TData>>;
1930
2665
  baseFilter?: DataBaseFilterHook | Record<string, DataBaseFilterHook>;
1931
2666
  overrideFilter?: DataOverrideFilterHook | Record<string, DataOverrideFilterHook>;
@@ -1933,7 +2668,7 @@ interface DataRouterOptions<TData = unknown> {
1933
2668
  decorateAll?: DataListHook<TData> | Record<string, DataListHook<TData>>;
1934
2669
  requestSchemas?: DataRequestSchemas;
1935
2670
  }
1936
- interface ExtendedModelRouterOptions<TModel = unknown> extends ModelRouterOptions<TModel>, ExtendedDefaultModelRouterOptions<TModel> {
2671
+ interface ExtendedModelRouterOptions<TModel = unknown, TVirtuals extends object = Record<never, never>> extends ModelRouterOptions<TModel, TVirtuals>, ExtendedDefaultModelRouterOptions<TModel> {
1937
2672
  'alwaysSelectFields.default'?: string[];
1938
2673
  'alwaysSelectFields.list'?: string[];
1939
2674
  'alwaysSelectFields.create'?: string[];
@@ -2015,8 +2750,29 @@ interface ExtendedModelRouterOptions<TModel = unknown> extends ModelRouterOption
2015
2750
  'defaults.publicReadOptions'?: PublicReadOptions;
2016
2751
  'defaults.publicUpdateArgs'?: PublicUpdateArgs;
2017
2752
  'defaults.publicUpdateOptions'?: PublicUpdateOptions;
2753
+ /**
2754
+ * Virtual dotted keys (VIRT-01): `virtuals`, `virtuals.<name>`,
2755
+ * `virtuals.<name>.<access>`, `virtuals.<field>.sub...` (recursive).
2756
+ * The template fallback preserves typed getter doc views for known
2757
+ * models so unknown persisted getter fields fail (including dotted
2758
+ * setters); embedded `sub` records stay structurally loose here and are
2759
+ * fully validated at runtime against the receiving Mongoose schema +
2760
+ * child scopes.
2761
+ */
2762
+ [key: `virtuals.${string}`]: ModelVirtuals<TModel, TVirtuals>[Extract<keyof TVirtuals, string>] | ModelVirtualDescriptor<TModel, unknown> | ModelVirtualGetter<TModel, unknown> | {
2763
+ sub?: Record<string, unknown>;
2764
+ } | Record<string, ModelVirtualDescriptor<Record<string, unknown>, unknown> | ModelVirtualGetter<Record<string, unknown>, unknown> | {
2765
+ sub?: unknown;
2766
+ }>;
2018
2767
  }
2019
2768
  interface ExtendedDataRouterOptions<TData = unknown> extends DataRouterOptions<TData> {
2769
+ 'operationAccess.default'?: Validation;
2770
+ 'operationAccess.list'?: Validation;
2771
+ 'operationAccess.read'?: Validation;
2772
+ 'operationAccess.basicList'?: Validation;
2773
+ 'operationAccess.advancedList'?: Validation;
2774
+ 'operationAccess.basicRead'?: Validation;
2775
+ 'operationAccess.advancedRead'?: Validation;
2020
2776
  'requestSchemas.advancedList'?: RequestSchema;
2021
2777
  'requestSchemas.advancedReadFilter'?: RequestSchema;
2022
2778
  'requestSchemas.advancedRead'?: RequestSchema;
@@ -2123,4 +2879,4 @@ declare function fromIoTs<TValue = unknown>(decoder: IoTsDecoderLike<TValue>): R
2123
2879
  declare function fromSuperstruct<TStruct, TOutput = unknown>(struct: TStruct, validate: SuperstructValidateLike): RequestSchemaValidator<TOutput>;
2124
2880
  declare function fromVine<TValue = unknown>(validator: VineValidatorLike<TValue>): RequestSchemaValidator<TValue>;
2125
2881
 
2126
- export { type DistinctArgs as $, type AccessRouterBaseRequest as A, type BaseFilterAccess as B, type Change as C, type CreateArgs as D, type CreateOptions as E, type CreateQueryInput as F, CustomHeaders as G, DATA_MIDDLEWARE as H, type DataBaseFilterHook as I, type DataFilter as J, type DataFindArgs as K, type DataFindOneArgs as L, type DataFindOneOptions as M, type DataFindOptions as N, type DataHook as O, type DataHookContext as P, type DataIdentifierHook as Q, type DataListHook as R, type DataOverrideFilterHook as S, type DataRequest as T, type DataRequestSchemas as U, type DataRouterOptions as V, type DecorateAccess as W, type DecorateAllAccess as X, type DeepFieldPath as Y, type DefaultModelRouterOptions as Z, type Defaults as _, type AccessRouterFieldKey as a, type PublicReadOptions as a$, type DistinctBody as a0, type DocPermissionsAccess as a1, type ErrorResult as a2, type ExistsOptions as a3, type ExtendedDataRouterOptions as a4, type ExtendedDefaultModelRouterOptions as a5, type ExtendedModelRouterOptions as a6, type Filter as a7, FilterOperator as a8, type FindAccess as a9, type ModelDocument as aA, type ModelDocumentHook as aB, type ModelHook as aC, type ModelHookContext as aD, type ModelIdentifierHook as aE, type ModelListHook as aF, type ModelOverrideFilterHook as aG, type ModelRequest as aH, type ModelRouterOptions as aI, type ModelValidateHook as aJ, PERMISSIONS as aK, PERMISSION_KEYS as aL, type ParentRef as aM, type PathValue as aN, type PermissionSchema as aO, type Populate as aP, type PopulateAccess as aQ, type PrepareAccess as aR, type Projection as aS, type PublicCreateArgs as aT, type PublicCreateOptions as aU, type PublicErrorResult as aV, type PublicListArgs as aW, type PublicListOptions as aX, type PublicListResult as aY, type PublicOutput as aZ, type PublicReadArgs as a_, type FindArgs as aa, type FindByIdArgs as ab, type FindByIdOptions as ac, type FindOneArgs as ad, type FindOneOptions as ae, type FindOptions as af, type GlobalOptions as ag, type GlobalPermissionValue as ah, type GuardHook as ai, type IdentifierHook as aj, type Include as ak, type IoTsContextEntryLike as al, type IoTsDecodeErrorLike as am, type IoTsDecoderLike as an, type JoiSchemaLike as ao, type JoiValidationErrorDetailLike as ap, type KeyValueProjection as aq, type LegacyInclude as ar, type ListQueryInput as as, type ListResult as at, MIDDLEWARE as au, type MaybePromise as av, type ModelBaseFilterHook as aw, type ModelChangeHook as ax, type ModelDeleteHook as ay, type ModelDocPermissionsHook as az, type AccessRouterLogger as b, type SubdocumentBulkRecord as b$, type PublicServiceResult as b0, type PublicSingleResult as b1, type PublicUpdateArgs as b2, type PublicUpdateOptions as b3, type PublicUpsertArgs as b4, type PublicUpsertOptions as b5, type ReadQueryInput as b6, type Request as b7, type RequestSchemaAdapter as b8, type RequestSchemaFailure as b9, type RootModelUpdateQueryEntry as bA, type RootModelUpsertQueryEntry as bB, type RootOperationResult as bC, type RootQueryEntry as bD, type RootRouterOptions as bE, type RootSubOperation as bF, type RootTarget as bG, type RouteGuardAccess as bH, type SelectAccess as bI, type SelectedPopulatedPublicOutput as bJ, type SelectedPublicOutput as bK, type ServiceResult as bL, type SingleResult as bM, type Sort as bN, type SortOrder as bO, type StandardSchemaFailure as bP, type StandardSchemaInferOutput as bQ, type StandardSchemaIssue as bR, type StandardSchemaPathSegment as bS, type StandardSchemaResult as bT, type StandardSchemaSuccess as bU, type StandardSchemaV1 as bV, StatusCodes as bW, type SubListBody as bX, type SubPopulate as bY, type SubQueryEntry as bZ, type SubReadBody as b_, type RequestSchemaIssue as ba, type RequestSchemaLike as bb, type RequestSchemaOptions as bc, type RequestSchemaResult as bd, type RequestSchemaSuccess as be, type RequestSchemaValidator as bf, type RequestSchemas as bg, type RootDataListQueryEntry as bh, type RootDataOperation as bi, type RootDataReadByFilterQueryEntry as bj, type RootDataReadByIdQueryEntry as bk, type RootModelCountQueryEntry as bl, type RootModelCreateQueryEntry as bm, type RootModelDeleteQueryEntry as bn, type RootModelDistinctQueryEntry as bo, type RootModelListQueryEntry as bp, type RootModelNewQueryEntry as bq, type RootModelOperation as br, type RootModelReadByFilterQueryEntry as bs, type RootModelReadByIdQueryEntry as bt, type RootModelSubBulkUpdateQueryEntry as bu, type RootModelSubCreateQueryEntry as bv, type RootModelSubDeleteQueryEntry as bw, type RootModelSubListQueryEntry as bx, type RootModelSubReadQueryEntry as by, type RootModelSubUpdateQueryEntry as bz, type AccessRouterRequest as c, type OpenApiResponses as c$, type SubdocumentBulkUpdateInput as c0, type SubdocumentCreateInput as c1, type SubdocumentCreateOptions as c2, type SubdocumentId as c3, type SubdocumentListOptions as c4, type SubdocumentName as c5, type SubdocumentParentArgs as c6, type SubdocumentParentOptions as c7, type SubdocumentReadOptions as c8, type SubdocumentRecord as c9, type YupValidationErrorLike as cA, defineRequestSchema as cB, fromAjv as cC, fromArkType as cD, fromIoTs as cE, fromJoi as cF, fromStandardSchema as cG, fromSuperstruct as cH, fromValibot as cI, fromVine as cJ, fromYup as cK, fromZod as cL, parseBody as cM, parseBodyWithSchema as cN, parseNestedBodyWithSchema as cO, parsePathParam as cP, parseQuery as cQ, AccessRuntime as cR, DataService as cS, Service as cT, PublicService as cU, type OpenApiRouterOptions as cV, type OpenApiRegistryOptions as cW, type OpenApiRouteDescriptor as cX, type OpenApiDocumentOptions as cY, type OpenApiParameter as cZ, type OpenApiSchema as c_, type SuperstructFailureLike as ca, type SuperstructValidateLike as cb, type Task as cc, type TaskType as cd, type TransformAccess as ce, type TypedFilter as cf, type UpdateByIdArgs as cg, type UpdateByIdOptions as ch, type UpdateOneArgs as ci, type UpdateOneOptions as cj, type UpdateQueryInput as ck, type UpsertArgs as cl, type UpsertOptions as cm, type UpsertQueryInput as cn, type ValibotIssueLike as co, type ValibotPathItemLike as cp, type ValibotSafeParseLike as cq, type ValibotSafeParseResult as cr, type ValidateAccess as cs, type ValidateRule as ct, type Validation as cu, type ValidationError as cv, type VineValidationErrorLike as cw, type VineValidationMessageLike as cx, type VineValidatorLike as cy, type YupSchemaLike as cz, type AccessRouterRequestExtensions as d, type AccessRouterPermissionMap as d0, type AccessRouterPermissions as d1, type OpenApiMethod as d2, type Permissions as d3, defaultRuntime as d4, type AdvancedCreateBody as e, type AdvancedListBody as f, type AdvancedReadBody as g, type AdvancedReadFilterBody as h, type AdvancedUpdateBody as i, type AdvancedUpsertBody as j, type AfterPersistAccess as k, type AjvErrorObjectLike as l, type AjvValidationErrorLike as m, type AjvValidatorLike as n, type ArkTypeErrorsLike as o, type ArkTypeLike as p, type ArkTypeProblemLike as q, Codes as r, type CorrelatedCountInclude as s, type CorrelatedFilter as t, type CorrelatedInclude as u, type CorrelatedIncludeArgs as v, type CorrelatedListInclude as w, type CorrelatedReadByFilterInclude as x, type CorrelatedReadByIdInclude as y, type CountBody as z };
2882
+ export { type DistinctArgs as $, type AccessRouterBaseRequest as A, type BaseFilterAccess as B, type Change as C, type CreateArgs as D, type CreateOptions as E, type CreateQueryInput as F, CustomHeaders as G, DATA_MIDDLEWARE as H, type DataBaseFilterHook as I, type DataFilter as J, type DataFindArgs as K, type DataFindOneArgs as L, type DataFindOneOptions as M, type DataFindOptions as N, type DataHook as O, type DataHookContext as P, type DataIdentifierHook as Q, type DataListHook as R, type DataOverrideFilterHook as S, type DataRequest as T, type DataRequestSchemas as U, type DataRouterOptions as V, type DecorateAccess as W, type DecorateAllAccess as X, type DeepFieldPath as Y, type DefaultModelRouterOptions as Z, type Defaults as _, type AccessRouterFieldKey as a, type Populate as a$, type DistinctBody as a0, type DocPermissionsAccess as a1, type ErrorResult as a2, type ExistsOptions as a3, type ExtendedDataRouterOptions as a4, type ExtendedDefaultModelRouterOptions as a5, type ExtendedModelRouterOptions as a6, type FieldOperationAccess as a7, type Filter as a8, FilterOperator as a9, type ModelDocPermissionsHook as aA, type ModelDocument as aB, type ModelDocumentHook as aC, type ModelHook as aD, type ModelHookContext as aE, type ModelIdentifierHook as aF, type ModelListHook as aG, type ModelOverrideFilterHook as aH, type ModelRequest as aI, type ModelRouterOptions as aJ, type ModelValidateHook as aK, type ModelVirtualAccess as aL, type ModelVirtualAccessRecord as aM, type ModelVirtualContext as aN, type ModelVirtualDescriptor as aO, type ModelVirtualDoc as aP, type ModelVirtualGetter as aQ, type ModelVirtualLeaf as aR, type ModelVirtualRecordAccess as aS, type ModelVirtuals as aT, type OperationAccess as aU, PERMISSIONS as aV, PERMISSION_KEYS as aW, type PairedRouteAccess as aX, type ParentRef as aY, type PathValue as aZ, type PermissionSchema as a_, type FindAccess as aa, type FindArgs as ab, type FindByIdArgs as ac, type FindByIdOptions as ad, type FindOneArgs as ae, type FindOneOptions as af, type FindOptions as ag, type GlobalOptions as ah, type GlobalPermissionValue as ai, type GuardHook as aj, type IdentifierHook as ak, type Include as al, type IoTsContextEntryLike as am, type IoTsDecodeErrorLike as an, type IoTsDecoderLike as ao, type JoiSchemaLike as ap, type JoiValidationErrorDetailLike as aq, type KeyValueProjection as ar, type LegacyInclude as as, type ListQueryInput as at, type ListResult as au, MIDDLEWARE as av, type MaybePromise as aw, type ModelBaseFilterHook as ax, type ModelChangeHook as ay, type ModelDeleteHook as az, type AccessRouterLogger as b, type SingleResult as b$, type PopulateAccess as b0, type PrepareAccess as b1, type Projection as b2, type PublicCreateArgs as b3, type PublicCreateOptions as b4, type PublicErrorResult as b5, type PublicListArgs as b6, type PublicListOptions as b7, type PublicListResult as b8, type PublicOutput as b9, type RootModelDistinctQueryEntry as bA, type RootModelListQueryEntry as bB, type RootModelNewQueryEntry as bC, type RootModelOperation as bD, type RootModelReadByFilterQueryEntry as bE, type RootModelReadByIdQueryEntry as bF, type RootModelSubBulkUpdateQueryEntry as bG, type RootModelSubCreateQueryEntry as bH, type RootModelSubDeleteQueryEntry as bI, type RootModelSubListQueryEntry as bJ, type RootModelSubReadQueryEntry as bK, type RootModelSubUpdateQueryEntry as bL, type RootModelUpdateQueryEntry as bM, type RootModelUpsertQueryEntry as bN, type RootOperationResult as bO, type RootQueryEntry as bP, type RootRouterOptions as bQ, type RootSubOperation as bR, type RootTarget as bS, type RouteBaseAccess as bT, type RouteGuardAccess as bU, type RouteVariant as bV, type RouteVariantAccess as bW, type SelectAccess as bX, type SelectedPopulatedPublicOutput as bY, type SelectedPublicOutput as bZ, type ServiceResult as b_, type PublicReadArgs as ba, type PublicReadOptions as bb, type PublicServiceResult as bc, type PublicSingleResult as bd, type PublicUpdateArgs as be, type PublicUpdateOptions as bf, type PublicUpsertArgs as bg, type PublicUpsertOptions as bh, type ReadQueryInput as bi, type Request as bj, type RequestSchemaAdapter as bk, type RequestSchemaFailure as bl, type RequestSchemaIssue as bm, type RequestSchemaLike as bn, type RequestSchemaOptions as bo, type RequestSchemaResult as bp, type RequestSchemaSuccess as bq, type RequestSchemaValidator as br, type RequestSchemas as bs, type RootDataListQueryEntry as bt, type RootDataOperation as bu, type RootDataReadByFilterQueryEntry as bv, type RootDataReadByIdQueryEntry as bw, type RootModelCountQueryEntry as bx, type RootModelCreateQueryEntry as by, type RootModelDeleteQueryEntry as bz, type AccessRouterRequest as c, fromValibot as c$, type Sort as c0, type SortOrder as c1, type StandardSchemaFailure as c2, type StandardSchemaInferOutput as c3, type StandardSchemaIssue as c4, type StandardSchemaPathSegment as c5, type StandardSchemaResult as c6, type StandardSchemaSuccess as c7, type StandardSchemaV1 as c8, StatusCodes as c9, type UpdateOneOptions as cA, type UpdateQueryInput as cB, type UpsertArgs as cC, type UpsertOptions as cD, type UpsertQueryInput as cE, type ValibotIssueLike as cF, type ValibotPathItemLike as cG, type ValibotSafeParseLike as cH, type ValibotSafeParseResult as cI, type ValidateAccess as cJ, type ValidateRule as cK, type Validation as cL, type ValidationError as cM, type VineValidationErrorLike as cN, type VineValidationMessageLike as cO, type VineValidatorLike as cP, type VirtualSubModel as cQ, type WithVirtuals as cR, type YupSchemaLike as cS, type YupValidationErrorLike as cT, defineRequestSchema as cU, fromAjv as cV, fromArkType as cW, fromIoTs as cX, fromJoi as cY, fromStandardSchema as cZ, fromSuperstruct as c_, type SubListBody as ca, type SubOperationAccess as cb, type SubPopulate as cc, type SubQueryEntry as cd, type SubReadBody as ce, type SubRouteGuardOptions as cf, type SubdocumentBulkRecord as cg, type SubdocumentBulkUpdateInput as ch, type SubdocumentCreateInput as ci, type SubdocumentCreateOptions as cj, type SubdocumentId as ck, type SubdocumentListOptions as cl, type SubdocumentName as cm, type SubdocumentParentArgs as cn, type SubdocumentParentOptions as co, type SubdocumentReadOptions as cp, type SubdocumentRecord as cq, type SuperstructFailureLike as cr, type SuperstructValidateLike as cs, type Task as ct, type TaskType as cu, type TransformAccess as cv, type TypedFilter as cw, type UpdateByIdArgs as cx, type UpdateByIdOptions as cy, type UpdateOneArgs as cz, type AccessRouterRequestExtensions as d, fromVine as d0, fromYup as d1, fromZod as d2, parseBody as d3, parseBodyWithSchema as d4, parseNestedBodyWithSchema as d5, parsePathParam as d6, parseQuery as d7, AccessRuntime as d8, DataService as d9, Service as da, PublicService as db, type OpenApiRouterOptions as dc, type OpenApiRegistryOptions as dd, type OpenApiRouteDescriptor as de, type OpenApiDocumentOptions as df, type OpenApiParameter as dg, type OpenApiSchema as dh, type OpenApiResponses as di, type AccessRouterPermissionMap as dj, type AccessRouterPermissions as dk, type OpenApiMethod as dl, type Permissions as dm, defaultRuntime as dn, type AdvancedCreateBody as e, type AdvancedListBody as f, type AdvancedReadBody as g, type AdvancedReadFilterBody as h, type AdvancedUpdateBody as i, type AdvancedUpsertBody as j, type AfterPersistAccess as k, type AjvErrorObjectLike as l, type AjvValidationErrorLike as m, type AjvValidatorLike as n, type ArkTypeErrorsLike as o, type ArkTypeLike as p, type ArkTypeProblemLike as q, Codes as r, type CorrelatedCountInclude as s, type CorrelatedFilter as t, type CorrelatedInclude as u, type CorrelatedIncludeArgs as v, type CorrelatedListInclude as w, type CorrelatedReadByFilterInclude as x, type CorrelatedReadByIdInclude as y, type CountBody as z };