# @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 ## 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. - `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`, `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