# @web-ts-toolkit/access-router-client Typed Axios-based client utilities for `@web-ts-toolkit/access-router` model routers, data routers, and root batch routes. ## Main Patterns ```ts import { createAdapter } from '@web-ts-toolkit/access-router-client'; const adapter = createAdapter({ baseURL: 'http://localhost:3000/api' }); const userService = adapter.createModelService({ modelName: 'User', basePath: 'users', }); const user = await userService.read('user-id-1'); if (user.success) { user.data.role = 'owner'; await user.data.save(); } ``` Data service for in-memory `access-router` data routes: ```ts const fruitService = adapter.createDataService({ dataName: 'fruit', basePath: 'fruit', }); ``` Batch grouped lazy requests into one root round trip: ```ts const grouped = await adapter.group( userService.readAdvanced('user-id-1', { select: ['name'] }), userService.countAdvanced({ role: 'admin' }), ); ``` Correlated includes (per-parent target queries; requires a correlated-include-capable server): ```ts import { parentField } from '@web-ts-toolkit/access-router-client'; const withOrg = await userService.readAdvanced('user-id-1', { include: [ orgService.readAdvanced(parentField('orgId'), { select: ['name'] }).$include('org'), postService.listAdvanced({ authorId: parentField('_id') }, { limit: 5 }).$include('posts'), postService.countAdvanced({ authorId: parentField('_id') }).$include('postCount'), ], }); ``` Notes: references resolve against the immediate parent on the outer server; plain `'$special'` strings stay literal (match a literal `{ $parent: 'x' }` object with `{ $escape: { $parent: 'x' } }`); reads attach `Out | null`, lists `Out[]`, counts `number` (missing/null refs attach `null`/`[]`/`0` with no target query); identifier reads keep the target's identifier behavior; list pagination applies per parent; conversion is synchronous with zero HTTP and reference-bearing calls return frozen non-thenable descriptors (convert with `$include(path)` — path-first generic `$include<'org', Org>('org')`); descriptors are transport-inert (inner queries always run on the outer server). ## Unreleased Migration - subdocument results are now plain data; replace subdocument `Model.save()` and `totalCount` usage with parent-scoped helper mutations and `SubDocumentListResponse.count` - subdocument create always returns the post-create array; model `create`/`createAdvanced` preserve scalar-versus-array input cardinality - narrow `Response` on `success`; failure `data` is `null` and structured errors live in `raw` - enabled caches are GET-only, identity-partitioned for credentials, and bounded to 100 LRU entries by default; caching remains disabled when `cacheTTL: 0` - each lazy request can execute directly or in one group, never both or repeatedly; grouped requests require one effective `throwOnError` policy, run all callbacks, and expose `{}` per-entry headers - remove data-service `includePermissions`, use string data sort, and call `countAdvanced(filter, config?)`; use the named filter escape hatches only for intentional dynamic/cast filters - model create/update/upsert payloads default to `ModelMutationInput` (`Partial`), and subdocument create/update payloads default to `SubDocumentMutationInput` (`Partial` for object subdocuments). Use `createModelService(...)` or `subs(...)` when request schemas differ from response shapes. `ModelData` is the direct-field data surface of a `Model` with reserved wrapper method names omitted - pass raw dynamic path values, reuse caller configs safely, and handle `MissingPersistenceIdentityError` when an existing projected model has no recoverable id - absent optional/projection-omitted nonreserved top-level assignments now track/save like `set`/`assign`; nested in-place edits still need `set` or `markModified`. Reserved wrapper/internal/inherited names and `then` are helper-only (`then` reads as undefined); direct writes throw TypeError. Model methods are owner-bound; fluent methods return the wrapper. - direct/assign keys must be nonempty literal strings without dot/brackets, forbidden prototype segments, or numeric spellings normalized to another key (e.g. `01`). Symbol writes/enumerable assign symbols reject; assign preflights all keys. Structural wrapper mutation (delete/defineProperty/prototype changes/freeze/seal) rejects. Use `toObject`/`toJSON` for data, not wrapper enumeration; reset removes added data and reads undefined even if an enumerable forwarder remains. Undefined is still omitted by JSON, not a server unset operation. Original/returned save wrappers remain independent. - cache keys separate partition/body components and body types; partition is captured at request time. Choose non-secret tokens for every response-affecting identity context. Deduplicated HTTP errors now share the transformed body in either caller order, with per-caller validateStatus/config; strict JSON parse and transport failures release slots for later requests. Service failure/throwOnError policy still applies. - generated service params preserve caller URLSearchParams order/duplicates on non-generated keys; generated keys replace all duplicates (nullish generated values omit keys, false/zero survive). Plain-object params keep Axios merge behavior. Grouped transport sends caller config once and generated options per body entry. - grouped config equality ignores plain-object key order/undefined properties and distinct URLSearchParams key order, but preserves array/repeated-value order. Date/URLSearchParams never equal tag-shaped plain records. Invalid/different configs reject before claims/dispatch, leaving requests directly executable. - query preparation rejects cycles, depth over 64 edges (root 0), or over 10,000 expanded visits with synchronous CorrelatedIncludeError and zero preparation HTTP. Count containers, primitives, array holes and repeated DAG occurrences. Applies to filter scanning/capture/copy/subquery rewriting, including supplemental filters: capture combines supplied id/filter/args/options; conversion combines id/rewritten filter and effective forwarded args. Undefined optional roots are absent; wire-envelope keys are outside the filter-only budget. `$escape` stays literal but structurally checked; live requests stay opaque until exposed `$$sq` metadata is checked. Caller containers are not frozen or lazy requests executed. - defaults separately allow 64 edges / 10,000 expanded visits: entire effective defaults bag is root 0, across all operations, including Date leaves/holes/repeated references. Direct service constructors check supplied defaults; adapter factories check the merged bag on service creation (not at createAdapter). Overridden fields and generated empty option bags do not count; merged valid bags can exceed the budget. Per-request selected default clones use fresh budgets. No request/frozen-input exemption; failures throw synchronous UnsupportedServiceDefaultValueError with `defaults.key[index]` paths (not a root export). Query conversion may still exceed its aggregate budget after defaults/subqueries expand. These are structural limits, not byte/latency or arbitrary getter/proxy/exotic-instance guarantees. ## Gotchas - supports **browsers and Node** (maintainer decision, ARC-19). Bundle target is `es2022`; `engines.node: ">=22"`, `browserslist: ["chrome >= 94", "edge >= 94", "firefox >= 93", "safari >= 16"]`. The source imports no Node built-ins; the cache `unref()` guard is feature-detected and a no-op in browsers. - `withCredentials: true` is the adapter default; in the browser this permits cookie credentials when CORS and cookie policy allow them. `Authorization`, proxy authorization, API-key style headers, and Node `Cookie` headers are explicit Axios config values; `withCredentials` does not create them. Credentialed caching still requires an explicit `cachePartition` token so one identity cannot receive another's cached response. - caching is disabled by default (`cacheTTL: 0`). `cacheTTL` is milliseconds. When enabled, only GET requests with supported JSON/text semantics cache or deduplicate, custom transforms/serializers and cancellation-sensitive requests bypass caching, and the default LRU capacity is 100 entries. - `axios` is a regular runtime dependency (declared in `package.json` `dependencies`); an installed consumer does not need to add axios separately. Use it as a peer only if you intentionally dedupe against an existing axios install. - service methods return lazy requests; they do not execute until `await`, `.then()`, `.exec()`, etc. - basic model `list(args?, options?, config?)` also accepts `list(options?, config?)`; `read(id, args?, options?, config?)` also keeps `read(id, options?, config?)`; `update(id, data, args?, options?, config?)` also keeps `update(id, data, options?, config?)`. Put select/pagination in args, execution flags in options, and Axios settings/throwOnError in config. Skipping args still applies listArgs/readArgs/updateArgs defaults. Both forms retain typed lazy requests, grouping and $include; basic select does not narrow the existing T/TData result type. Separate args/options objects; mixed keys throw. Empty ambiguous objects keep the original form (list args-first, read/update options-first); the explicit final config position selects the full form. - basic model list `args.sort` accepts the general Sort string/object/tuple forms and sends a single signed-field GET query value (`'name -age'`). Objects/tuples preserve ordering priority; grouped metadata/$include keep original forms. listArgs.sort defaults apply even with args omitted. Empty sorts (`''`/[]/{}) send sort= to suppress backend defaults. GET server field authorization, sortableFields and stripDisallowedSort apply. - `adapter.group(...)` only batches service lazy requests from the same adapter, not raw Axios calls or consumer-created `wrapLazyPromise(...)` values - grouped requests must have one effective `throwOnError` policy after per-call/service/adapter precedence; mixed policies reject before dispatch. Non-throwing groups return all partial-failure entries. Throwing groups invoke every executed entry callback once, then reject with the first failed entry's `ServiceError`. Group entry `headers` are `{}` because root responses do not carry per-operation headers - the client `basePath` is relative to the adapter `baseURL`, not the full server path - match `queryPath`/`mutationPath` to the server-side `queryRouteSegment` and mutation route configuration - each dynamic URL path segment (identifiers, `distinct` field, subdocument `id`/`sub`/`subId`, wrapper `pathParams` values) is `encodeURIComponent`-encoded exactly once; already-encoded inputs are re-encoded so a single server-side decode returns the literal input - caller-owned `axiosRequestConfig`, headers (including `AxiosHeaders` instances), and wrapper default configs are never mutated by service methods or wrap helpers; the same config object can be reused across many requests without acquiring hidden cache controls or `params` - `Model` reserves public wrapper member names (`save`, `reset`, `set`, `get`, `assign`, `toObject`, `toJSON`, etc.) for methods on direct property access; document fields with those names remain available through `get(...)`, `set(...)`, `assign(...)`, and `toObject()` - multiple overlapping `save()` calls on the same `Model` instance are serialized in call order; a queued save snapshots dirty paths only after the previous save finishes reconciling - the client request contract mirrors the sibling `@web-ts-toolkit/access-router` server: - `distinctAdvanced(field, filter, ...)` sends `{ filter }` as the request body, not the bare conditions, so the server honors the filter - `DataService` does not advertise `includePermissions` — the data routers do not parse `include_permissions`; data records are returned without `_permissions` - `DataService` `listAdvanced.args.sort` is `string` (`'age'` / `'-age'`), not the general `Sort` union rejected by the server - `ModelService.update(...)` and `ModelService.upsert(...)` accept `includePermissions` and transmit it as the `include_permissions` query parameter - `countAdvanced(filter, axiosRequestConfig?)` no longer accepts an `access` argument the server rejects - `SubDocumentListResponse` carries `count` (the server's field), not `totalCount`; `ListModelResponse` still carries `totalCount` - subdocument `create(data | data[], ...)` accepts a single object or an array; the response is always the post-create subdocument array - `distinct(field, ...)` / `distinctAdvanced(field, filter, ...)` return `Response` with no server-value stringification; narrow elements (e.g. `typeof v === 'string'`) before calling string methods - service defaults accept null, string, boolean, finite numbers, valid `Date` values (detached per request), plain objects, and arrays; anything else throws `UnsupportedServiceDefaultValueError`. `sq` in adapter/service defaults applies to `list`, `listAdvanced`, `read`, `readAdvanced`, and `readAdvancedFilter` with per-call, service, then adapter precedence - `Response` is a discriminated union (`SuccessResult | FailureResult`); branch on `result.success` to narrow `raw`/`data`. On `success: false`, `data` is always `null` and `raw` is the unknown (or opt-in typed) server error payload. Only model/data list responses carry `totalCount`; subdocument list-like responses carry `count`; scalar and single responses carry neither - this package targets `@web-ts-toolkit/access-router` servers; plain Axios is simpler for non-access-router APIs - imports are named-only (`import { createAdapter }`); there is no default export - the public export surface is locked by `access-router-client.exports.unit.test.ts`. Only these names are part of the supported root API: - runtime values: `createAdapter`, `ModelService`, `DataService`, `Service`, `ServiceError`, `MissingPersistenceIdentityError`, `Model`, `CustomHeaders`, `wrapLazyPromise`, `replaceItemById`, `removeItemById`, `parentField`, `CorrelatedIncludeError` - type/interface exports (named via `import type`): `AdapterOptions`, `ModelServiceOptions`, `DataServiceOptions`, `CacheController`, `CachePartitioner`, `Response`, `SuccessResult`, `FailureResult`, `ModelResponse`, `ArrayModelResponse`, `ListModelResponse`, `ModelData`, `DataResponse`, `ArrayDataResponse`, `ListDataResponse`, `SubDocumentResponse`, `SubDocumentListResponse`, `Document`, `ModelMutationInput`, `SubDocumentMutationInput`, `Projection`, `KeyValueProjection`, `SelectedKeys`, `SelectedShape`, `ResolvedSelectedShape`, `Sort`, `SortOrder`, `FilterQuery`, `DottedPathFilter`, `ServerSideCast`, `Populate`, `PopulateAccess`, `Include`, `Task`, `SubQueryOptions`, `WrapOptions`, `ResultError`, `ResponseCallback`, `AdditionalReqConfig`, `Defaults`, `DataDefaults`, `LazyRequest`, `ModelRequest`, `DataRequest`, `ModelPromiseMeta`, `DataPromiseMeta`, `RootModelQueryMeta`, `RootDataQueryMeta`, `RootQueryMeta`, `ListArgs`, `ListOptions`, `ListAdvancedArgs`, `ListAdvancedOptions`, `ReadArgs`, `ReadOptions`, `ReadAdvancedArgs`, `ReadAdvancedOptions`, `CreateOptions`, `CreateAdvancedArgs`, `CreateAdvancedOptions`, `UpdateOptions`, `UpdateAdvancedArgs`, `UpdateAdvancedOptions`, `UpsertOptions`, `UpsertAdvancedArgs`, `UpsertAdvancedOptions`, `DataListArgs`, `DataListOptions`, `DataListAdvancedArgs`, `DataListAdvancedOptions`, `DataReadOptions`, `DataReadAdvancedArgs`, `DataReadAdvancedOptions`, `ParentRef`, `CorrelatedInclude`, `CorrelatedIncludeOp`, `CorrelatedIncludeArgs`, `CorrelatedIncludeInput`, `CorrelatedFilterQuery`, `CorrelatedQuerySelector`, `EscapeLiteral`, `SupplementalIncludeOptions`, `WithCorrelatedOutputs`, `IncludableRead`, `IncludableList`, `IncludableCount`, `IncludableBasicList`, `IncludableBasicCount`, `CorrelatedReadDescriptor`, `CorrelatedListDescriptor`, `CorrelatedCountDescriptor` Model `create(...)` and `createAdvanced(...)` preserve cardinality: object input returns `ModelResponse`; array input returns `ArrayModelResponse`, even for one item. - any name not listed above is implementation-internal and must not be relied on. Configure caching through `AdapterOptions` (`cacheTTL`, `cachePartition`, `cacheCapacity`); control an existing cache through the adapter's `clearCache()` and `disposeCache()` methods; configure `throwOnError` per-service or per-call rather than reaching for `applyResponseCallbacks` directly. ## Pointers - README: installation, quickstart, main exports, browser+Node runtime matrix - website (not packed into the npm tarball; use the live URLs after install): full documentation online at https://web-ts-toolkit.pages.dev/docs/packages/access-router-client (adapter, services, model, typing and errors) - `pnpm --filter @web-ts-toolkit/access-router-client test:browser-smoke`: jsdom+Vite smoke test that imports the built bundle; this is not a real-browser engine/version gate