@web-ts-toolkit/access-router-client 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -98,12 +98,16 @@ consumer needs:
98
98
  defaults to `'__mutation'`; `rootRouterPath` defaults to `'root'`.
99
99
  - **Cache & authentication policy:** credentialed requests are **never**
100
100
  cached unless `cachePartition` returns a stable, non-secret identity token.
101
+ Browser cookie credentials, explicit `Authorization`/proxy authorization
102
+ headers, API-key style headers, and Node `Cookie` headers supplied on the
103
+ request config are all treated as credentialed.
101
104
  Sensitive headers (`authorization`, `cookie`, `set-cookie`,
102
105
  `proxy-authorization`, `www-authenticate`) are excluded from cache keys
103
106
  regardless of the partition token. Only GET requests with supported JSON or
104
107
  text response semantics are cached; mutations and custom transforms or
105
- serializers always bypass caching. `cacheTTL: 0` (the default) disables the
106
- cache entirely, while enabled caches retain at most 100 entries by default.
108
+ serializers always bypass caching. `cacheTTL` is measured in milliseconds;
109
+ `cacheTTL: 0` (the default) disables the cache entirely, while enabled caches
110
+ retain at most 100 entries by default.
107
111
  `clearCache()` drops every cached entry; `disposeCache()`
108
112
  drops entries and releases cache timers (call on adapter teardown so timers
109
113
  do not keep a Node process alive).
@@ -139,6 +143,15 @@ consumer needs:
139
143
  - **Model create cardinality:** `create(...)` and `createAdvanced(...)` accept
140
144
  either one object or an array. Scalar input returns `ModelResponse<T>`;
141
145
  array input returns `ArrayModelResponse<T>`, including for a one-item array.
146
+ - **Mutation input types:** model create/update/upsert payloads default to
147
+ `ModelMutationInput<T>` (`Partial<T>`), so object literals are checked for
148
+ known field names and scalar types without pretending the server can infer
149
+ required create fields. Pass `createModelService<T, TCreateInput,
150
+ TUpdateInput, TUpsertInput>(...)` when request schemas differ from the
151
+ response model. Subdocument create/update helpers use
152
+ `SubDocumentMutationInput<S>` (`Partial<S>` for object subdocuments) by
153
+ default and can be customized through `subs<S, K, TCreateInput,
154
+ TUpdateInput>(...)`.
142
155
  - **Nested model edits:** `Model<T>` tracks modified top-level paths and
143
156
  reconciles writes against the last loaded/saved snapshot. Direct mutation
144
157
  of nested objects/arrays (`obj.arr.push(...)`, `obj.sub.field = x`) is
@@ -146,12 +159,18 @@ consumer needs:
146
159
  or `markModified('topLevelField')` after a direct mutation (forces dirty
147
160
  without reconciling). Reverting a value to its snapshot clears the dirty
148
161
  flag. `save()` persists only tracked modified top-level fields; if `_id`
149
- exists it calls `update(...)`, otherwise it calls `create(...)`.
162
+ exists it calls `update(...)`, otherwise it calls `create(...)`. Multiple
163
+ overlapping `save()` calls on the same wrapper are serialized in call order.
164
+ Document fields named like model methods (`save`, `reset`, `set`, `get`,
165
+ `assign`, `toJSON`, etc.) are reserved for the wrapper API on direct
166
+ property access; use `get(...)`, `set(...)`, `assign(...)`, or `toObject()`
167
+ for those data fields. The exported `ModelData<T>` helper reflects that
168
+ reserved-name contract in response and `Model.create(...)` types.
150
169
  - **Supported runtimes:** Node 22+ and modern evergreen browsers (see
151
170
  [Supported Runtimes](#supported-runtimes) and
152
171
  [Browser And Node Support](#browser-and-node-support) above).
153
172
 
154
- ## Main Exports
173
+ ## Primary Exports
155
174
 
156
175
  The package is named-export-only (no default export). Import every public
157
176
  symbol from the package root:
@@ -174,9 +193,9 @@ import {
174
193
  // Thrown instead of creating a duplicate when an existing projected model
175
194
  // has no recoverable persistence identity.
176
195
  MissingPersistenceIdentityError,
177
- // Lazy-promise wrapper with non-enumerable metadata and a single
178
- // shared execution. Used internally by service methods; exported so
179
- // consumers can build compatible lazy promises for custom batches.
196
+ // Low-level lazy-promise wrapper with a single shared execution. Service
197
+ // methods add private adapter metadata required by `adapter.group(...)`;
198
+ // consumer-created wrappers execute directly and are not groupable.
180
199
  wrapLazyPromise,
181
200
  // Normalized response-count / pagination header names.
182
201
  CustomHeaders,
@@ -202,6 +221,7 @@ import type {
202
221
  ModelResponse,
203
222
  ArrayModelResponse,
204
223
  ListModelResponse,
224
+ ModelData,
205
225
  DataResponse,
206
226
  ArrayDataResponse,
207
227
  ListDataResponse,
@@ -213,6 +233,8 @@ import type {
213
233
  DataDefaults,
214
234
  // Filter, projection, populate, sort, and request-meta primitives.
215
235
  FilterQuery,
236
+ ModelMutationInput,
237
+ SubDocumentMutationInput,
216
238
  DottedPathFilter,
217
239
  ServerSideCast,
218
240
  Projection,
@@ -247,6 +269,7 @@ type StablePublicTypes = [
247
269
  ModelResponse<Document>,
248
270
  ArrayModelResponse<Document>,
249
271
  ListModelResponse<Document>,
272
+ ModelData<Document>,
250
273
  DataResponse<unknown>,
251
274
  ArrayDataResponse<unknown>,
252
275
  ListDataResponse<unknown>,
@@ -255,6 +278,8 @@ type StablePublicTypes = [
255
278
  Defaults,
256
279
  DataDefaults,
257
280
  FilterQuery<Document>,
281
+ ModelMutationInput<Document>,
282
+ SubDocumentMutationInput<{ label: string }>,
258
283
  DottedPathFilter<Document>,
259
284
  ServerSideCast<Document>,
260
285
  Projection,
@@ -265,9 +290,9 @@ type StablePublicTypes = [
265
290
  void (null as unknown as StablePublicTypes);
266
291
  ```
267
292
 
268
- Only the names above are part of the stable public surface. The package
269
- ships a runtime export contract test (`access-router-client.exports.unit.test.ts`)
270
- that fails on accidental additions or removals, so implementation internals
293
+ The names above are the primary public imports most consumers need. The full
294
+ root export inventory is locked by `access-router-client.exports.unit.test.ts`
295
+ and mirrored in `llms.txt`, so implementation internals
271
296
  such as `useCacheInterceptors`, `cloneConfigWithCacheBypass`,
272
297
  `finalizeRootEntry`, `applyGroupCallbacks`, `makeRequest`, `createWrapHelper`,
273
298
  `ADAPTER_ID_KEY`, `STARTED_KEY`, `CACHE_HEADER`, `CachePolicy`, and `RootEntry`
@@ -278,29 +303,27 @@ through the returned adapter's `clearCache()` and `disposeCache()` methods.
278
303
  ## Browser And Node Support
279
304
 
280
305
  - **Bundle target:** `es2022` (see `tsup.config.ts`). The single shared target
281
- runs in Node 22+ and all evergreen browsers without transpilation; the
282
- source imports no Node built-ins.
306
+ runs in Node 22+ and the documented evergreen browser floor without
307
+ transpilation; the source imports no Node built-ins.
283
308
  - **Runtime metadata:** `engines.node: ">=22"` (npm/pnpm warn or refuse on
284
- older Node) and `browserslist: ["supports es2022-module"]` (bundler tools
285
- narrow to the same matrix). Unsupported environments fail clearly via
286
- engine warnings rather than appearing accidentally supported.
309
+ older Node) and `browserslist: ["chrome >= 94", "edge >= 94", "firefox >= 93", "safari >= 16"]`.
310
+ `pnpm exec browserslist` resolves this package config without error.
287
311
  - **Authentication contract:** `withCredentials: true` is the adapter
288
- default, so the browser runtime transmits cookies + the `Authorization`
289
- header and the cache partitions credentialed requests via the
290
- `cachePartition` option (see [Cache Controls](#cache-controls)). In Node,
291
- `withCredentials` is honored by Axios's HTTP adapter the same way for
292
- `Cookie` headers you set manually; credentialed caching still requires an
293
- explicit `cachePartition` token so one identity cannot receive another's
294
- cached response.
312
+ default, so browser requests may include cookies when CORS and cookie policy
313
+ allow them. `Authorization`, proxy authorization, API-key style headers, and
314
+ Node `Cookie` headers are explicit Axios config values; `withCredentials`
315
+ does not create them. Credentialed caching still requires an explicit
316
+ `cachePartition` token so one identity cannot receive another's cached
317
+ response.
295
318
  - **Cache timers:** the in-memory cache uses `setTimeout`/`clearTimeout`
296
319
  (available in both runtimes). The optional Node `unref()` guard is
297
320
  feature-detected and is a no-op in browsers, so `clearCache()` and
298
321
  `disposeCache()` are safe to call in either runtime.
299
322
  - **Smoke test:** `pnpm --filter @web-ts-toolkit/access-router-client
300
323
  test:browser-smoke` (powered by Vite + jsdom) imports the _built_
301
- `dist/index.mjs` under a browser environment and exercises the public
302
- runtime surface. It fails if a Node built-in leaks into the bundle or the
303
- bundle emits syntax the declared `browserslist` floor cannot run. This
324
+ `dist/index.mjs` under a browser-like environment and exercises the public
325
+ runtime surface. It is a smoke check for Node built-in leaks and basic ESM
326
+ browser bundling, not a real-browser engine/version compatibility gate. This
304
327
  smoke test also runs as part of the default `pnpm test` for the package.
305
328
 
306
329
  ## Documentation
package/index.d.mts CHANGED
@@ -207,7 +207,7 @@ declare class Service {
207
207
  private _wrap;
208
208
  private _throwOnError;
209
209
  constructor(axios: AxiosInstance, basePath: string, throwOnError?: boolean);
210
- protected handleSuccess(res: AxiosResponse<unknown, unknown>, extra?: {}): Response<unknown>;
210
+ protected handleSuccess<T extends Response<unknown, unknown> = Response<unknown>>(res: AxiosResponse<unknown, unknown>, extra?: {}): T;
211
211
  protected handleError<T extends Response<unknown, unknown>>(error: unknown): Extract<T, FailureResult>;
212
212
  /** Resolves per-call policy against the already-resolved service/adapter default. */
213
213
  resolveThrowOnError(override?: boolean): boolean;
@@ -271,7 +271,8 @@ interface Props$1 {
271
271
  * const userService = adapter.createModelService<User>({ modelName: 'User', basePath: 'users' });
272
272
  * const user = await userService.read('user-id-1');
273
273
  */
274
- declare class ModelService<T extends Document> extends Service {
274
+ type InferredSubDocument<T, K extends keyof T, S> = [S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S;
275
+ declare class ModelService<T extends Document, TCreateInput extends object = ModelMutationInput<T>, TUpdateInput extends object = ModelMutationInput<T>, TUpsertInput extends object = ModelMutationInput<T>> extends Service {
275
276
  private _modelName;
276
277
  private _queryPath;
277
278
  private _mutationPath;
@@ -280,12 +281,12 @@ declare class ModelService<T extends Document> extends Service {
280
281
  constructor({ axios, modelName, basePath, queryPath, mutationPath, onSuccess, onFailure, throwOnError }: Props$1, defaults?: Defaults);
281
282
  list<TData extends Partial<T> = T>(args?: ListArgs, options?: ListOptions, axiosRequestConfig?: RequestConfig$1): ModelPromiseMeta & LazyRequest<ListModelResponse<T, TData>>;
282
283
  listAdvanced<TData extends Partial<T> | never = never, TSelect extends Projection = Projection>(filter: FilterQuery<T>, args?: ListAdvancedArgs<TSelect>, options?: ListAdvancedOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ListModelResponse<T, ResolvedSelectedShape<T, TSelect, TData>>>;
283
- create<TData extends Partial<T> = T>(data: object[], options?: CreateOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ArrayModelResponse<T, TData>>;
284
- create<TData extends Partial<T> = T>(data: object, options?: CreateOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ModelResponse<T, TData>>;
285
- createAdvanced<TData extends Partial<T> | never = never, TSelect extends Projection = Projection>(data: object[], args?: CreateAdvancedArgs<TSelect>, options?: CreateAdvancedOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ArrayModelResponse<T, ResolvedSelectedShape<T, TSelect, TData>>>;
286
- createAdvanced<TData extends Partial<T> | never = never, TSelect extends Projection = Projection>(data: object, args?: CreateAdvancedArgs<TSelect>, options?: CreateAdvancedOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ModelResponse<T, ResolvedSelectedShape<T, TSelect, TData>>>;
287
- upsert<TData extends Partial<T> = T>(data: object, options?: UpsertOptions, axiosRequestConfig?: RequestConfig$1): ModelPromiseMeta & LazyRequest<ModelResponse<T, TData>>;
288
- upsertAdvanced<TData extends Partial<T> | never = never, TSelect extends Projection = Projection>(data: object, args?: UpsertAdvancedArgs<TSelect>, options?: UpsertAdvancedOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ModelResponse<T, ResolvedSelectedShape<T, TSelect, TData>>>;
284
+ create<TData extends Partial<T> = T>(data: TCreateInput[], options?: CreateOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ArrayModelResponse<T, TData>>;
285
+ create<TData extends Partial<T> = T>(data: TCreateInput, options?: CreateOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ModelResponse<T, TData>>;
286
+ createAdvanced<TData extends Partial<T> | never = never, TSelect extends Projection = Projection>(data: TCreateInput[], args?: CreateAdvancedArgs<TSelect>, options?: CreateAdvancedOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ArrayModelResponse<T, ResolvedSelectedShape<T, TSelect, TData>>>;
287
+ createAdvanced<TData extends Partial<T> | never = never, TSelect extends Projection = Projection>(data: TCreateInput, args?: CreateAdvancedArgs<TSelect>, options?: CreateAdvancedOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ModelResponse<T, ResolvedSelectedShape<T, TSelect, TData>>>;
288
+ upsert<TData extends Partial<T> = T>(data: TUpsertInput, options?: UpsertOptions, axiosRequestConfig?: RequestConfig$1): ModelPromiseMeta & LazyRequest<ModelResponse<T, TData>>;
289
+ upsertAdvanced<TData extends Partial<T> | never = never, TSelect extends Projection = Projection>(data: TUpsertInput, args?: UpsertAdvancedArgs<TSelect>, options?: UpsertAdvancedOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ModelResponse<T, ResolvedSelectedShape<T, TSelect, TData>>>;
289
290
  delete(identifier: string, axiosRequestConfig?: RequestConfig$1): ModelPromiseMeta & LazyRequest<Response<string>>;
290
291
  new<TData extends Partial<T> = T>(axiosRequestConfig?: RequestConfig$1): ModelPromiseMeta & LazyRequest<ModelResponse<T, TData>>;
291
292
  distinct(field: string, axiosRequestConfig?: RequestConfig$1): ModelPromiseMeta & LazyRequest<Response<string[]>>;
@@ -295,36 +296,36 @@ declare class ModelService<T extends Document> extends Service {
295
296
  read<TData extends Partial<T> = T>(identifier: string, options?: ReadOptions, axiosRequestConfig?: RequestConfig$1): ModelPromiseMeta & LazyRequest<ModelResponse<T, TData>>;
296
297
  readAdvanced<TData extends Partial<T> | never = never, TSelect extends Projection = Projection>(identifier: string, args?: ReadAdvancedArgs<TSelect>, options?: ReadAdvancedOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ModelResponse<T, ResolvedSelectedShape<T, TSelect, TData>>>;
297
298
  readAdvancedFilter<TData extends Partial<T> | never = never, TSelect extends Projection = Projection>(filter: FilterQuery<T>, args?: ReadAdvancedArgs<TSelect>, options?: ReadAdvancedOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ModelResponse<T, ResolvedSelectedShape<T, TSelect, TData>>>;
298
- update<TData extends Partial<T> = T>(identifier: string, data: object, options?: UpdateOptions, axiosRequestConfig?: RequestConfig$1): ModelPromiseMeta & LazyRequest<ModelResponse<T, TData>>;
299
- updateAdvanced<TData extends Partial<T> | never = never, TSelect extends Projection = Projection>(identifier: string, data: object, args?: UpdateAdvancedArgs<TSelect>, options?: UpdateAdvancedOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ModelResponse<T, ResolvedSelectedShape<T, TSelect, TData>>>;
299
+ update<TData extends Partial<T> = T>(identifier: string, data: TUpdateInput, options?: UpdateOptions, axiosRequestConfig?: RequestConfig$1): ModelPromiseMeta & LazyRequest<ModelResponse<T, TData>>;
300
+ updateAdvanced<TData extends Partial<T> | never = never, TSelect extends Projection = Projection>(identifier: string, data: TUpdateInput, args?: UpdateAdvancedArgs<TSelect>, options?: UpdateAdvancedOptions, axiosRequestConfig?: RequestConfig$1): ModelRequest<ModelResponse<T, ResolvedSelectedShape<T, TSelect, TData>>>;
300
301
  id(id: string): {
301
- subs: <S = never, K extends keyof T = keyof T>(field: K) => {
302
+ subs: <S = never, K extends keyof T = keyof T, TSubCreateInput = SubDocumentMutationInput<InferredSubDocument<T, K, S>>, TSubUpdateInput = SubDocumentMutationInput<InferredSubDocument<T, K, S>>>(field: K) => {
302
303
  list: (axiosRequestConfig?: AxiosRequestConfig<any, any> & {
303
304
  throwOnError?: boolean;
304
- }) => ModelPromiseMeta & LazyRequest<SubDocumentListResponse<[S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S>>;
305
- listAdvanced: <TData extends Partial<[S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S> = never, TSelect extends readonly string[] = readonly string[]>(filter?: FilterQuery<[S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S>, args?: {
305
+ }) => ModelPromiseMeta & LazyRequest<SubDocumentListResponse<InferredSubDocument<T, K, S>>>;
306
+ listAdvanced: <TData extends Partial<InferredSubDocument<T, K, S>> = never, TSelect extends readonly string[] = readonly string[]>(filter?: FilterQuery<InferredSubDocument<T, K, S>>, args?: {
306
307
  select?: TSelect;
307
308
  }, axiosRequestConfig?: AxiosRequestConfig<any, any> & {
308
309
  throwOnError?: boolean;
309
- }) => ModelPromiseMeta & LazyRequest<SubDocumentListResponse<[S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S, ResolvedSelectedShape<[S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S, TSelect, TData>>>;
310
+ }) => ModelPromiseMeta & LazyRequest<SubDocumentListResponse<InferredSubDocument<T, K, S>, ResolvedSelectedShape<InferredSubDocument<T, K, S>, TSelect, TData>>>;
310
311
  read: (subId: string, axiosRequestConfig?: AxiosRequestConfig<any, any> & {
311
312
  throwOnError?: boolean;
312
- }) => ModelPromiseMeta & LazyRequest<SubDocumentResponse<[S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S>>;
313
- readAdvanced: <TData extends Partial<[S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S> = never, TSelect_1 extends readonly string[] = readonly string[]>(subId: string, args?: {
313
+ }) => ModelPromiseMeta & LazyRequest<SubDocumentResponse<InferredSubDocument<T, K, S>>>;
314
+ readAdvanced: <TData extends Partial<InferredSubDocument<T, K, S>> = never, TSelect_1 extends readonly string[] = readonly string[]>(subId: string, args?: {
314
315
  select?: TSelect_1;
315
316
  populate?: unknown;
316
317
  }, axiosRequestConfig?: AxiosRequestConfig<any, any> & {
317
318
  throwOnError?: boolean;
318
- }) => ModelPromiseMeta & LazyRequest<SubDocumentResponse<[S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S, ResolvedSelectedShape<[S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S, TSelect_1, TData>>>;
319
- update: (subId: string, data: object, axiosRequestConfig?: AxiosRequestConfig<any, any> & {
319
+ }) => ModelPromiseMeta & LazyRequest<SubDocumentResponse<InferredSubDocument<T, K, S>, ResolvedSelectedShape<InferredSubDocument<T, K, S>, TSelect_1, TData>>>;
320
+ update: (subId: string, data: TSubUpdateInput, axiosRequestConfig?: AxiosRequestConfig<any, any> & {
320
321
  throwOnError?: boolean;
321
- }) => ModelPromiseMeta & LazyRequest<SubDocumentResponse<[S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S>>;
322
- bulkUpdate: (data: object[], axiosRequestConfig?: AxiosRequestConfig<any, any> & {
322
+ }) => ModelPromiseMeta & LazyRequest<SubDocumentResponse<InferredSubDocument<T, K, S>>>;
323
+ bulkUpdate: (data: TSubUpdateInput[], axiosRequestConfig?: AxiosRequestConfig<any, any> & {
323
324
  throwOnError?: boolean;
324
- }) => ModelPromiseMeta & LazyRequest<SubDocumentListResponse<[S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S>>;
325
- create: (data: object | object[], axiosRequestConfig?: AxiosRequestConfig<any, any> & {
325
+ }) => ModelPromiseMeta & LazyRequest<SubDocumentListResponse<InferredSubDocument<T, K, S>>>;
326
+ create: (data: TSubCreateInput | TSubCreateInput[], axiosRequestConfig?: AxiosRequestConfig<any, any> & {
326
327
  throwOnError?: boolean;
327
- }) => ModelPromiseMeta & LazyRequest<SubDocumentListResponse<[S] extends [never] ? NonNullable<T[K]> extends readonly (infer TItem)[] ? TItem : never : S>>;
328
+ }) => ModelPromiseMeta & LazyRequest<SubDocumentListResponse<InferredSubDocument<T, K, S>>>;
328
329
  delete: (subId: string, axiosRequestConfig?: AxiosRequestConfig<any, any> & {
329
330
  throwOnError?: boolean;
330
331
  }) => ModelPromiseMeta & LazyRequest<Response<string>>;
@@ -380,16 +381,19 @@ declare class MissingPersistenceIdentityError extends Error {
380
381
  /**
381
382
  * A dirty-tracking wrapper around a model document. Constructed via
382
383
  * {@link ModelService.create}, {@link ModelService.read},
383
- * {@link ModelService.findOne}, or the list methods that return
384
+ * {@link ModelService.readAdvanced}, or the list methods that return
384
385
  * `Model<T>[]`. Property access through the wrapper directly reads/writes
385
386
  * the underlying data; `save()` persists only the paths flagged dirty
386
387
  * since the last save and merges the server response per the documented
387
388
  * concurrency contract.
388
389
  *
389
- * `Model.create<T>(data, service)` is typed as `Model<T, TData> & TData` so
390
- * callers can read/write fields directly on the wrapper (`user.role =
391
- * 'owner'`) while still calling `save()`/`reset()`/`isDirty(...)`. The
392
- * returned wrapper is a fresh snapshot of the post-operation local state;
390
+ * `Model.create<T>(data, service)` is typed as `Model<T, TData> &
391
+ * ModelData<T, TData>` so callers can read/write ordinary fields directly on
392
+ * the wrapper (`user.role = 'owner'`) while still calling
393
+ * `save()`/`reset()`/`isDirty(...)`. Fields whose names collide with public
394
+ * model methods or properties are reserved for the wrapper API and remain
395
+ * reachable through `get(...)`, `set(...)`, `assign(...)`, and `toObject()`.
396
+ * The returned wrapper is a fresh snapshot of the post-operation local state;
393
397
  * mutating it does not affect sibling wrappers created from the same
394
398
  * underlying document.
395
399
  *
@@ -411,32 +415,37 @@ declare class Model<T extends Document, TData extends Partial<T> = T> {
411
415
  private _snapshot;
412
416
  private readonly _service;
413
417
  private modifiedPaths;
418
+ private _saveQueue;
414
419
  private _persistenceId;
415
420
  private readonly _fromExisting;
416
421
  constructor(data: TData, adapter: ModelService<T>, persistenceId?: string, fromExisting?: boolean);
417
- static create<T extends Document, TData extends Partial<T> = T>(data: TData, adapter: ModelService<T>, persistenceId?: string, fromExisting?: boolean): Model<T, TData> & TData;
422
+ static create<T extends Document, TData extends Partial<T> = T>(data: TData, adapter: ModelService<T>, persistenceId?: string, fromExisting?: boolean): Model<T, TData> & ModelData<T, TData>;
418
423
  /**
419
424
  * Persists the currently dirty paths to the server, then merges the
420
425
  * server's response back into local state.
421
426
  *
422
427
  * Concurrency contract:
423
428
  *
424
- * 1. Submitted paths and their values are snapshotted before the request
429
+ * 1. Multiple `save()` calls on the same wrapper are serialized in call
430
+ * order. A later save snapshots its dirty paths only after the previous
431
+ * save has finished reconciling, so overlapping callers cannot submit
432
+ * the same stale dirty set concurrently.
433
+ * 2. Submitted paths and their values are snapshotted before the request
425
434
  * starts, so an in-flight response cannot wipe edits that were made
426
435
  * while the request was pending.
427
- * 2. On success, a submitted path is cleared from `modifiedPaths` only if
436
+ * 3. On success, a submitted path is cleared from `modifiedPaths` only if
428
437
  * its current local value still equals the submitted value — i.e. the
429
438
  * user has not concurrently re-edited it to a different value.
430
- * 3. Server-returned values overwrite local values for paths the user did
439
+ * 4. Server-returned values overwrite local values for paths the user did
431
440
  * NOT concurrently re-modify during the in-flight save; for paths the
432
441
  * user did concurrently re-modify, the local value is preserved and
433
442
  * the dirty flag is retained so the concurrent edit is resubmitted on
434
443
  * the next `save()`. (Deterministic conflict rule: the newer local
435
444
  * edit wins for the same path; the server value becomes its reset
436
445
  * baseline without replacing the newer local value.)
437
- * 4. On failure, no dirty state is cleared and no local value is
446
+ * 5. On failure, no dirty state is cleared and no local value is
438
447
  * overwritten; the caller can retry `save()` with the same set.
439
- * 5. The return value echoes `{ ...result, data }` where `data` is a
448
+ * 6. The return value echoes `{ ...result, data }` where `data` is a
440
449
  * refreshed `Model` snapshot of the post-save local state (or `null`
441
450
  * on failure), matching `ModelResponse<T, TData>`.
442
451
  *
@@ -452,6 +461,7 @@ declare class Model<T extends Document, TData extends Partial<T> = T> {
452
461
  * of POSTing a new document.
453
462
  */
454
463
  save(reqConfig?: AxiosRequestConfig): Promise<ModelResponse<T, TData>>;
464
+ private saveNow;
455
465
  isDirty(path?: keyof TData | string): boolean;
456
466
  /**
457
467
  * Marks a path dirty and skips snapshot reconciliation. This is the
@@ -495,6 +505,16 @@ declare class Model<T extends Document, TData extends Partial<T> = T> {
495
505
  */
496
506
  private reconcilePath;
497
507
  }
508
+ /**
509
+ * Directly exposed data fields for a `Model` wrapper.
510
+ *
511
+ * Runtime property forwarding reserves public `Model` member names such as
512
+ * `save`, `reset`, `set`, `get`, `assign`, `toObject`, and `toJSON` for the
513
+ * wrapper API. Documents may still contain those field names, but callers must
514
+ * access them via `get(...)`, `set(...)`, `assign(...)`, or `toObject()` rather
515
+ * than ordinary direct property access.
516
+ */
517
+ type ModelData<T extends Document, TData extends Partial<T> = T> = Omit<TData, keyof Model<T, TData>>;
498
518
 
499
519
  type AnyArray<T> = T[] | ReadonlyArray<T>;
500
520
  type Unpacked<T> = T extends (infer U)[] ? U : T extends ReadonlyArray<infer U> ? U : T;
@@ -512,7 +532,8 @@ type Unpacked<T> = T extends (infer U)[] ? U : T extends ReadonlyArray<infer U>
512
532
  * (dynamic dotted paths and explicit server-side casting).
513
533
  */
514
534
  type ApplyBasicQueryCasting<T> = T | T[] | (T extends AnyArray<unknown> ? Unpacked<T> : never) | (T extends string ? RegExp : never);
515
- type Condition<T> = ApplyBasicQueryCasting<T> | QuerySelector<ApplyBasicQueryCasting<T>> | LazyRequest<unknown>;
535
+ type QueryOperatorOperand<T> = T extends AnyArray<unknown> ? Unpacked<T> : T;
536
+ type Condition<T> = ApplyBasicQueryCasting<T> | QuerySelector<T> | LazyRequest<unknown>;
516
537
  type _FilterQuery<T> = {
517
538
  [P in keyof T]?: Condition<T[P]>;
518
539
  } & RootQuerySelector<T>;
@@ -536,15 +557,15 @@ type RootQuerySelector<T> = {
536
557
  $comment?: string;
537
558
  };
538
559
  type QuerySelector<T> = {
539
- $eq?: T;
540
- $gt?: T;
541
- $gte?: T;
542
- $in?: [T] extends AnyArray<unknown> ? Unpacked<T>[] : T[];
543
- $lt?: T;
544
- $lte?: T;
545
- $ne?: T;
546
- $nin?: [T] extends AnyArray<unknown> ? Unpacked<T>[] : T[];
547
- $not?: T extends string ? QuerySelector<T> | RegExp : QuerySelector<T>;
560
+ $eq?: ApplyBasicQueryCasting<T>;
561
+ $gt?: QueryOperatorOperand<T>;
562
+ $gte?: QueryOperatorOperand<T>;
563
+ $in?: QueryOperatorOperand<T>[];
564
+ $lt?: QueryOperatorOperand<T>;
565
+ $lte?: QueryOperatorOperand<T>;
566
+ $ne?: ApplyBasicQueryCasting<T>;
567
+ $nin?: QueryOperatorOperand<T>[];
568
+ $not?: QueryOperatorOperand<T> extends string ? QuerySelector<T> | RegExp : QuerySelector<T>;
548
569
  /**
549
570
  * When `true`, `$exists` matches the documents that contain the field,
550
571
  * including documents where the field value is null.
@@ -553,9 +574,9 @@ type QuerySelector<T> = {
553
574
  $type?: string | number;
554
575
  $expr?: unknown;
555
576
  $jsonSchema?: unknown;
556
- $mod?: T extends number ? [number, number] : never;
557
- $regex?: T extends string ? RegExp | string : never;
558
- $options?: T extends string ? string : never;
577
+ $mod?: QueryOperatorOperand<T> extends number ? [number, number] : never;
578
+ $regex?: QueryOperatorOperand<T> extends string ? RegExp | string : never;
579
+ $options?: QueryOperatorOperand<T> extends string ? string : never;
559
580
  };
560
581
  /**
561
582
  * Escape hatch for dynamic dotted paths and explicit server-side casting.
@@ -648,6 +669,19 @@ interface Populate {
648
669
  interface Document {
649
670
  _id?: string;
650
671
  }
672
+ /**
673
+ * Default request payload type for model mutations.
674
+ *
675
+ * The sibling access-router runtime accepts generic records and does not know
676
+ * a consumer application's required create/update schema. The client therefore
677
+ * defaults mutation inputs to `Partial<T>` so known fields are checked without
678
+ * claiming compile-time requiredness. Consumers with distinct request schemas
679
+ * can pass explicit `ModelService<T, TCreateInput, TUpdateInput, TUpsertInput>`
680
+ * or `createModelService<T, ...>(...)` generics.
681
+ */
682
+ type ModelMutationInput<T extends Document> = Partial<T>;
683
+ /** Default request payload type for subdocument create/update helpers. */
684
+ type SubDocumentMutationInput<T> = T extends object ? Partial<T> : T;
651
685
  /**
652
686
  * Successful response. `raw` and `data` are non-null and `success` is
653
687
  * narrowed to `true` so `if (result.success)` exposes the documented
@@ -689,8 +723,8 @@ interface FailureResult<TError = unknown> {
689
723
  * type unless a caller explicitly chooses that error type.
690
724
  */
691
725
  type Response<T1, T2 = T1, TError = unknown> = SuccessResult<T1, T2> | FailureResult<TError>;
692
- type ModelResponse<T extends Document, TData extends Partial<T> = T> = Response<TData, Model<T, TData> & TData>;
693
- type ArrayModelResponse<T extends Document, TData extends Partial<T> = T> = Response<TData[], (Model<T, TData> & TData)[]>;
726
+ type ModelResponse<T extends Document, TData extends Partial<T> = T> = Response<TData, Model<T, TData> & ModelData<T, TData>>;
727
+ type ArrayModelResponse<T extends Document, TData extends Partial<T> = T> = Response<TData[], (Model<T, TData> & ModelData<T, TData>)[]>;
694
728
  /**
695
729
  * `ListModelResponse` always carries `totalCount` on both branches. The field
696
730
  * defaults to `0` at runtime on failure or when the server did not emit count
@@ -828,11 +862,12 @@ type CachePartitioner = (config: InternalAxiosRequestConfig) => string | undefin
828
862
  *
829
863
  * Cache controls (only in effect when `cacheTTL > 0`):
830
864
  *
831
- * - `cacheTTL` — seconds a cached GET response is reused before revalidation.
865
+ * - `cacheTTL` — milliseconds a cached GET response is reused before revalidation.
832
866
  * - `cachePartition` — required to cache credentialed requests safely (see
833
- * {@link CachePartitioner}); credentialed requests without a stable,
834
- * non-secret partition token bypass the cache so one identity cannot
835
- * receive a response created under another.
867
+ * {@link CachePartitioner}); requests using browser cookies,
868
+ * `withCredentials`, or explicit auth headers without a stable, non-secret
869
+ * partition token bypass the cache so one identity cannot receive a
870
+ * response created under another.
836
871
  * - `cacheCapacity` — bounds the number of cached entries; defaults to 100 and
837
872
  * evicts the LRU entry when the limit is exceeded.
838
873
  *
@@ -847,11 +882,11 @@ interface AdapterOptions {
847
882
  throwOnError?: boolean;
848
883
  cacheTTL?: number;
849
884
  /**
850
- * Partition strategy for credentialed cache entries. When the adapter is
851
- * credentialed (which is the default), caching is only enabled for requests
852
- * whose `cachePartition` returns a stable, non-secret identity token. Requests
853
- * without a partition key bypass the cache so that one identity can never
854
- * receive a response created under another identity.
885
+ * Partition strategy for credentialed cache entries. When a request uses
886
+ * browser cookies, `withCredentials`, or explicit auth headers, caching is
887
+ * only enabled when `cachePartition` returns a stable, non-secret identity
888
+ * token. Requests without a partition key bypass the cache so that one
889
+ * identity can never receive a response created under another identity.
855
890
  *
856
891
  * The returned value must be a stable, non-secret token (for example a user
857
892
  * id or tenant id). Never return raw cookies, authorization values, or other
@@ -935,7 +970,7 @@ declare function createAdapter(axiosConfig?: AxiosRequestConfig, adapterOptions?
935
970
  axios: axios.AxiosInstance;
936
971
  clearCache: () => void;
937
972
  disposeCache: () => void;
938
- createModelService: <T extends Document>({ modelName, basePath, queryPath, mutationPath, onSuccess, onFailure, throwOnError, }: ModelServiceOptions, defaults?: Defaults) => ModelService<T>;
973
+ createModelService: <T extends Document, TCreateInput extends object = Partial<T>, TUpdateInput extends object = Partial<T>, TUpsertInput extends object = Partial<T>>({ modelName, basePath, queryPath, mutationPath, onSuccess, onFailure, throwOnError, }: ModelServiceOptions, defaults?: Defaults) => ModelService<T, TCreateInput, TUpdateInput, TUpsertInput>;
939
974
  createDataService: <T>({ dataName, basePath, queryPath, onSuccess, onFailure, throwOnError }: DataServiceOptions, defaults?: DataDefaults) => DataService<T>;
940
975
  wrapGet: <T = unknown>(url: string, defaultConfig?: AxiosRequestConfig) => (options?: WrapOptions, requestConfig?: AxiosRequestConfig) => Promise<axios.AxiosResponse<T, any, {}, any>>;
941
976
  wrapPost: <T = unknown>(url: string, defaultConfig?: AxiosRequestConfig) => (data?: unknown, options?: WrapOptions, requestConfig?: AxiosRequestConfig) => Promise<axios.AxiosResponse<T, any, {}, any>>;
@@ -964,4 +999,4 @@ declare function removeItemById<T extends {
964
999
  _id: string;
965
1000
  }>(items: T[], targetItem: T): T[];
966
1001
 
967
- export { type AdapterOptions, type AdditionalReqConfig, type ArrayDataResponse, type ArrayModelResponse, type CacheController, type CachePartitioner, type CreateAdvancedArgs, type CreateAdvancedOptions, type CreateOptions, CustomHeaders, type DataDefaults, type DataListAdvancedArgs, type DataListAdvancedOptions, type DataListArgs, type DataListOptions, type DataPromiseMeta, type DataReadAdvancedArgs, type DataReadAdvancedOptions, type DataReadOptions, type DataRequest, type DataResponse, DataService, type DataServiceOptions, type Defaults, type Document, type DottedPathFilter, type FailureResult, type FilterQuery, type Include, type KeyValueProjection, type LazyRequest, type ListAdvancedArgs, type ListAdvancedOptions, type ListArgs, type ListDataResponse, type ListModelResponse, type ListOptions, MissingPersistenceIdentityError, Model, type ModelPromiseMeta, type ModelRequest, type ModelResponse, ModelService, type ModelServiceOptions, type Populate, type PopulateAccess, type Projection, type ReadAdvancedArgs, type ReadAdvancedOptions, type ReadOptions, type ResolvedSelectedShape, type Response, type ResponseCallback, type ResultError, type RootDataQueryMeta, type RootModelQueryMeta, type RootQueryMeta, type SelectedKeys, type SelectedShape, type ServerSideCast, Service, ServiceError, type Sort, type SortOrder, type SubDocumentListResponse, type SubDocumentResponse, type SubQueryOptions, type SuccessResult, type Task, type UpdateAdvancedArgs, type UpdateAdvancedOptions, type UpdateOptions, type UpsertAdvancedArgs, type UpsertAdvancedOptions, type UpsertOptions, type WrapOptions, createAdapter, removeItemById, replaceItemById, wrapLazyPromise };
1002
+ export { type AdapterOptions, type AdditionalReqConfig, type ArrayDataResponse, type ArrayModelResponse, type CacheController, type CachePartitioner, type CreateAdvancedArgs, type CreateAdvancedOptions, type CreateOptions, CustomHeaders, type DataDefaults, type DataListAdvancedArgs, type DataListAdvancedOptions, type DataListArgs, type DataListOptions, type DataPromiseMeta, type DataReadAdvancedArgs, type DataReadAdvancedOptions, type DataReadOptions, type DataRequest, type DataResponse, DataService, type DataServiceOptions, type Defaults, type Document, type DottedPathFilter, type FailureResult, type FilterQuery, type Include, type KeyValueProjection, type LazyRequest, type ListAdvancedArgs, type ListAdvancedOptions, type ListArgs, type ListDataResponse, type ListModelResponse, type ListOptions, MissingPersistenceIdentityError, Model, type ModelData, type ModelMutationInput, type ModelPromiseMeta, type ModelRequest, type ModelResponse, ModelService, type ModelServiceOptions, type Populate, type PopulateAccess, type Projection, type ReadAdvancedArgs, type ReadAdvancedOptions, type ReadOptions, type ResolvedSelectedShape, type Response, type ResponseCallback, type ResultError, type RootDataQueryMeta, type RootModelQueryMeta, type RootQueryMeta, type SelectedKeys, type SelectedShape, type ServerSideCast, Service, ServiceError, type Sort, type SortOrder, type SubDocumentListResponse, type SubDocumentMutationInput, type SubDocumentResponse, type SubQueryOptions, type SuccessResult, type Task, type UpdateAdvancedArgs, type UpdateAdvancedOptions, type UpdateOptions, type UpsertAdvancedArgs, type UpsertAdvancedOptions, type UpsertOptions, type WrapOptions, createAdapter, removeItemById, replaceItemById, wrapLazyPromise };