@haddowg/json-api-client 1.0.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.
@@ -0,0 +1,1254 @@
1
+ //#region src/descriptor.d.ts
2
+ /**
3
+ * The generated runtime descriptor (ADR 0001). The codegen emits one of these per
4
+ * server `as const satisfies ApiDescriptor`; the client's TYPES are derived from it
5
+ * (one source of truth). It carries everything the generic runtime needs that types
6
+ * alone can't: attribute-vs-relation, cardinality, related type(s), per-operation
7
+ * path templates (uriType/prefix-aware), paginator kind, and the create client-id
8
+ * policy.
9
+ */
10
+ type Cardinality = 'one' | 'many';
11
+ /**
12
+ * The mutation verbs a single relationship endpoint advertises (derived from the HTTP
13
+ * methods it exposes). These are NOT uniform across relations — the bundle gates them
14
+ * per relation (`cannotAdd`/`cannotRemove`/`cannotReplace`, per-relation endpoint
15
+ * exposure) — so the descriptor carries the capability per relation and the fluent
16
+ * surface gates `add`/`remove`/`replace`/`set` on it (see ADR 0001 follow-up).
17
+ *
18
+ * For a to-many: `add` <- POST, `remove` <- DELETE, `replace` <- PATCH.
19
+ * For a to-one: `set` <- PATCH. A flag is `true` iff the endpoint advertises the verb.
20
+ */
21
+ interface RelationMutations {
22
+ add?: boolean;
23
+ remove?: boolean;
24
+ replace?: boolean;
25
+ set?: boolean;
26
+ }
27
+ interface RelationDescriptor {
28
+ cardinality: Cardinality;
29
+ /** Related JSON:API type(s); more than one = polymorphic. */
30
+ types: readonly string[];
31
+ /** Whether members carry pivot data (`meta.pivot`). */
32
+ pivot?: boolean;
33
+ /**
34
+ * The pivot fields carried on each member's `meta.pivot` (a `belongsToMany` relation), as a
35
+ * field-name → wire-format-hint map (the same hint vocabulary as {@link ResourceDescriptor.attributes}).
36
+ * Types `$pivot` precisely (`{ position: number; weight: number; addedAt: string }`) instead of a
37
+ * loose `Record<string, unknown>` (D33). Absent when the relation carries no pivot.
38
+ */
39
+ pivotFields?: Readonly<Record<string, string>>;
40
+ /**
41
+ * `false` when the related endpoint (`GET /{type}/{id}/{rel}`) is suppressed for this relation
42
+ * (the bundle's `withoutRelatedEndpoint()`, ADR 0027) — `.related()` is then a compile error /
43
+ * runtime guard rather than a 404. Absent means exposed (the default).
44
+ */
45
+ related?: boolean;
46
+ /**
47
+ * `false` when the relationship endpoint (`.../relationships/{rel}`) is suppressed for this
48
+ * relation (the bundle's `withoutRelationshipEndpoint()`, ADR 0027) — `.get()` (and every
49
+ * mutation) is then gated off. Absent means exposed (the default).
50
+ */
51
+ relationship?: boolean;
52
+ /**
53
+ * The per-relation mutation verbs the relationship endpoint advertises. An explicit `{}` means
54
+ * the endpoint exposes no mutation verb (gates every verb OFF). Absent means the descriptor
55
+ * carries no signal (a hand-written descriptor) — gating then falls back to cardinality alone.
56
+ * A codegen descriptor always emits this (at least `{}`) so suppression never fails open.
57
+ */
58
+ mutations?: RelationMutations;
59
+ /**
60
+ * The `withCount` count tokens + negotiation profile this relation's related/relationship reads
61
+ * advertise (`_self_` + the relation's own countable relations). Lets `.related()`/`.get()` send
62
+ * `withCount` and negotiate the right profile; absent when the relation advertises no Countable
63
+ * profile (D3).
64
+ */
65
+ countable?: CountableDescriptor;
66
+ /**
67
+ * The paginator kind this relation's OWN related/relationship endpoints advertise
68
+ * (`GET /{type}/{id}/{rel}` / `.../relationships/{rel}`), when it DIVERGES from the related
69
+ * type's collection-level {@link ResourceDescriptor.paginator}. A related endpoint may paginate
70
+ * differently than the related type's own collection (e.g. a cursor-paginated related to-many
71
+ * whose target type is page-paginated at its top-level collection) — so the client resolves a
72
+ * related/embedded to-many array's `$page.kind` from this per-relation value first, falling back
73
+ * to the related type's collection paginator (D6a). Absent when the two agree (the common case).
74
+ */
75
+ paginator?: PaginatorKind;
76
+ }
77
+ type PaginatorKind = 'page' | 'offset' | 'cursor' | 'none';
78
+ type ClientIdPolicy = 'forbidden' | 'optional' | 'required';
79
+ /** Where a custom action is invoked: collection-scoped (`/{type}/-actions/{name}`) or resource-scoped (`/{type}/{id}/-actions/{name}`). */
80
+ type ActionScope = 'collection' | 'resource';
81
+ /** The body a custom action accepts: a JSON:API document, no body, or a raw (non-JSON:API) payload. */
82
+ type ActionInput = 'document' | 'none' | 'raw';
83
+ /**
84
+ * What a custom action returns: a JSON:API document (materialised into the resource view of its
85
+ * {@link ActionDescriptor.outputType}), a meta-only document (its top-level `meta`), or nothing (a
86
+ * `204`).
87
+ */
88
+ type ActionOutput = 'document' | 'meta' | 'none';
89
+ /**
90
+ * A custom action declared on a type (`#[AsJsonApiAction]`). The runtime reaches it via
91
+ * `client.<type>.actions.<name>(...)` (collection scope) or
92
+ * `client.<type>.id(id).actions.<name>(...)` (resource scope); `input`/`output` drive how the body
93
+ * is sent and the response shaped (see CONTEXT.md "Write surface").
94
+ */
95
+ interface ActionDescriptor {
96
+ scope: ActionScope;
97
+ /** The action path template (e.g. `/albums/{id}/-actions/reissue`). */
98
+ path: string;
99
+ /**
100
+ * The HTTP method the action is invoked over, upper-case (e.g. `PATCH`). Absent means `POST`
101
+ * (the default) — so an action declaring a single non-POST method (`#[AsJsonApiAction(methods:
102
+ * ['PATCH'])]`) stays reachable rather than being silently dropped.
103
+ */
104
+ method?: string;
105
+ input: ActionInput;
106
+ output: ActionOutput;
107
+ /**
108
+ * The JSON:API type of a `document`-input action's body — lets the client accept FLAT input
109
+ * (like `create`) and build the envelope + remap `422` pointers, rather than the caller
110
+ * hand-building `{ data: { type, attributes } }`. Absent for `none`/`raw` inputs, or a bespoke
111
+ * command document whose type isn't a registered resource (then the raw envelope is passed
112
+ * through).
113
+ */
114
+ inputType?: string;
115
+ /**
116
+ * The JSON:API type of a `document`-output action's primary resource — the client materialises
117
+ * the response into that type's resource view (matching the read path), so the typed result is
118
+ * `result.title`, not `result.data.attributes.title`. Absent for `meta`/`none` outputs.
119
+ */
120
+ outputType?: string;
121
+ /** Whether a `document`-output action returns a single resource (`one`) or a collection (`many`). */
122
+ outputCardinality?: Cardinality;
123
+ /**
124
+ * The declared request media type for a `raw`-input action (e.g. `application/octet-stream`),
125
+ * sent verbatim as the `Content-Type` so the server accepts the non-JSON:API body. Absent for
126
+ * `document` (the JSON:API media type) and `none` (no body) inputs.
127
+ */
128
+ contentType?: string;
129
+ }
130
+ /**
131
+ * The `withCount` capability of a type's read endpoints (the Countable profile, core ADR
132
+ * 0101). `tokens` are the count tokens a `withCount` query may carry (`_self_` counts the
133
+ * collection itself; a relation name counts that relation per item); `profile` is the URI a
134
+ * client must negotiate (in `Accept`) before the server honours `withCount` — else it is
135
+ * rejected (400) under strict query-param validation. Absent when no read endpoint of the
136
+ * type advertises `withCount`.
137
+ */
138
+ interface CountableDescriptor {
139
+ tokens: readonly string[];
140
+ profile: string;
141
+ }
142
+ interface ResourceDescriptor {
143
+ /** Attribute name -> wire format hint (drives optional value coercion). */
144
+ attributes: Readonly<Record<string, string>>;
145
+ relations: Readonly<Record<string, RelationDescriptor>>;
146
+ /** Per-operation path templates, e.g. `{ fetchOne: '/albums/{id}' }`. */
147
+ paths: Readonly<Record<string, string>>;
148
+ paginator: PaginatorKind;
149
+ clientId: ClientIdPolicy;
150
+ /**
151
+ * The `withCount` count tokens + negotiation profile for this type's read endpoints.
152
+ * Absent when no read endpoint advertises `withCount`.
153
+ */
154
+ countable?: CountableDescriptor;
155
+ /**
156
+ * The relation paths the read endpoints accept in `include` — the exact enum the OpenAPI
157
+ * document advertises, including nested dotted paths (`tracks.album`). Drives `include`
158
+ * narrowing. Absent when the type advertises no includable relations (so `include` is then
159
+ * a compile error, matching the server's `400 INCLUSION_NOT_ALLOWED`).
160
+ */
161
+ includable?: readonly string[];
162
+ /**
163
+ * The sort tokens the COLLECTION read accepts in `sort` — signed field names (`title`,
164
+ * `-title`). Drives `sort` narrowing. Absent when the collection advertises no sorting (so
165
+ * `sort` is then a compile error, matching the server's `400 SORTING_UNSUPPORTED`).
166
+ */
167
+ sortable?: readonly string[];
168
+ /**
169
+ * The filter keys the COLLECTION read accepts in `filter[...]`. Drives `filter` key narrowing
170
+ * (values stay `unknown` — value shapes/operators vary per filter, out of scope for v0.1).
171
+ * Absent when the collection advertises no filters (so any `filter` is then a compile error,
172
+ * matching the server's `400 FILTERING_UNRECOGNIZED`).
173
+ */
174
+ filterable?: readonly string[];
175
+ /** Custom actions declared on this type, keyed by action name. Absent/empty when none. */
176
+ actions?: Readonly<Record<string, ActionDescriptor>>;
177
+ }
178
+ type ApiDescriptor = Readonly<Record<string, ResourceDescriptor>>;
179
+ /**
180
+ * The server-level Atomic Operations capability (ADR 0001): the single `/operations`
181
+ * endpoint, detected when its requestBody uses the atomic ext media type. `null` when the
182
+ * server exposes no atomic endpoint. Carried alongside the per-type descriptor (not inside
183
+ * it — atomic is server-level, not per-type) so the runtime `client.atomic` builder can
184
+ * reach the path.
185
+ */
186
+ interface AtomicDescriptor {
187
+ path: string;
188
+ }
189
+ //#endregion
190
+ //#region src/materialise.d.ts
191
+ /** A trimmed, by-reference top-level envelope shared by every resource from one response. */
192
+ interface DocumentEnvelope {
193
+ jsonapi?: Record<string, unknown>;
194
+ meta?: Record<string, unknown>;
195
+ links?: Record<string, unknown>;
196
+ }
197
+ /** Resource-level links (`self` is the common one; the wire may carry more). */
198
+ interface ResourceLinks {
199
+ self?: string;
200
+ [key: string]: unknown;
201
+ }
202
+ /** The relationship-instance envelope for a materialised related value (its `$edge`). */
203
+ interface Edge {
204
+ links?: Record<string, unknown>;
205
+ meta?: Record<string, unknown>;
206
+ }
207
+ /** Normalised pagination, discriminated by paginator kind; count-free-safe. */
208
+ interface Page {
209
+ kind: PaginatorKind;
210
+ /** The raw `meta.page` block, when present (shape varies by kind). */
211
+ meta?: Record<string, unknown>;
212
+ /** Navigation links by relation (`first`/`prev`/`next`/`last`), when present. */
213
+ links: {
214
+ first?: string;
215
+ prev?: string;
216
+ next?: string;
217
+ last?: string;
218
+ };
219
+ }
220
+ /** The uniform shape `$rel(name)` returns. */
221
+ interface RelationView {
222
+ data?: unknown;
223
+ links?: Record<string, unknown>;
224
+ meta?: Record<string, unknown>;
225
+ }
226
+ /** Non-enumerable envelope accessors shared by every materialised resource. */
227
+ interface ResourceEnvelope {
228
+ readonly $meta: Record<string, unknown> | undefined;
229
+ readonly $links: ResourceLinks | undefined;
230
+ readonly $self: string | undefined;
231
+ readonly $document: DocumentEnvelope;
232
+ readonly $raw: RawResource;
233
+ $rel(name: string): RelationView | undefined;
234
+ }
235
+ /** Edge-local accessors carried by a materialised related VALUE (per-edge view). */
236
+ interface EdgeEnvelope {
237
+ readonly $edge: Edge | undefined;
238
+ readonly $pivot: Record<string, unknown> | undefined;
239
+ }
240
+ /** Relationship-level accessors carried by an augmented to-many array. */
241
+ interface ArrayEnvelope {
242
+ readonly $page: Page;
243
+ readonly $links: Record<string, unknown> | undefined;
244
+ readonly $meta: Record<string, unknown> | undefined;
245
+ $next(): Promise<unknown>;
246
+ $prev(): Promise<unknown>;
247
+ }
248
+ /** The runtime context materialise reads its descriptor + navigation seam from. */
249
+ interface MaterialiseContext {
250
+ descriptor: ApiDescriptor;
251
+ /**
252
+ * Re-fetch + re-materialise a link (wired in Build 4); returns the materialised value.
253
+ * `linkage` propagates the originating surface so a paginated relationship-endpoint
254
+ * array re-materialises as identifiers (not resources) on `$next()`/`$prev()`. `paginator`
255
+ * carries the current page's kind so the re-materialised page reports the same `$page.kind`
256
+ * (a divergent per-relation paginator, or an EMPTY next page that carries no member to sniff).
257
+ */
258
+ navigate: (url: string, linkage?: boolean, paginator?: PaginatorKind) => Promise<unknown>;
259
+ /**
260
+ * The opt-in per-resource validator (ADR 0004), resolved from `ClientOptions.validate`. When
261
+ * present, every wire resource in `data`/`included` is validated against its per-type schema
262
+ * before the document is materialised. Absent => no per-field validation (the default).
263
+ */
264
+ validate?: ResolvedValidator | undefined;
265
+ }
266
+ /** A raw JSON:API resource object (or bare identifier) off the wire. */
267
+ interface RawResource {
268
+ type: string;
269
+ id: string;
270
+ meta?: Record<string, unknown>;
271
+ links?: ResourceLinks;
272
+ attributes?: Record<string, unknown>;
273
+ relationships?: Record<string, RawRelationship>;
274
+ }
275
+ interface RawRelationship {
276
+ data?: unknown;
277
+ links?: Record<string, unknown>;
278
+ meta?: Record<string, unknown>;
279
+ }
280
+ /**
281
+ * Materialise a JSON:API document into a hydrated graph (ADR 0002 / CONTEXT.md):
282
+ *
283
+ * - single resource -> a flattened resource object (own enumerable type/id/attributes/
284
+ * relations + non-enumerable `$`-accessors);
285
+ * - collection / related collection -> an augmented array of resource objects;
286
+ * - relationship linkage -> an augmented array of identifier views (to-many) or a single
287
+ * identifier / null (to-one).
288
+ *
289
+ * Included resources hydrate into nested per-edge views (reads through to the shared
290
+ * node for attributes, owns its own `$edge`/`$pivot`); linked-but-not-included relations
291
+ * stay as identifiers; links-only relations are `undefined`. The envelope rides
292
+ * non-enumerable `$`-accessors so `{...res}` / `JSON.stringify(res)` are clean.
293
+ *
294
+ * `linkage` discriminates a relationship-endpoint response (primary `data` is pure
295
+ * resource-identifier linkage) from a resource/collection response (primary `data` is a
296
+ * full resource) — a structural guess can't tell them apart for an attribute-less,
297
+ * relation-less resource, so the caller passes the surface it issued.
298
+ *
299
+ * `paginatorOverride` sets the top-level array's `$page.kind` for a related/relationship read,
300
+ * where the paginator is the RELATION's (which may diverge from the related type's collection —
301
+ * D6a) and an empty page carries no member to sniff a type from. `primaryType` is preferred for a
302
+ * top-level collection read (statically-known); both override the sniff-from-`data[0]` fallback.
303
+ */
304
+ declare function materialise(doc: Document, ctx: MaterialiseContext, linkage?: boolean, primaryType?: string, paginatorOverride?: PaginatorKind): unknown;
305
+ /**
306
+ * A to-many relation's paginator kind. The relation's OWN related/relationship endpoint may
307
+ * paginate differently than the related type's top-level collection (D6a), so prefer the
308
+ * per-relation `paginator` (carried in the descriptor only when it diverges); otherwise fall back
309
+ * to the related type's own paginator (the relationship and related endpoints both paginate that
310
+ * type), so the discriminant is identical across the three surfaces. A polymorphic relation
311
+ * resolves off its first declared related type (its members span types, matching the relationship
312
+ * endpoint's first-member resolution).
313
+ */
314
+ declare function relatedPaginatorKind(relation: RelationDescriptor | undefined, ctx: MaterialiseContext): PaginatorKind;
315
+ //#endregion
316
+ //#region src/types.d.ts
317
+ /** The JSON:API media type. */
318
+ declare const JSON_API_MEDIA_TYPE = "application/vnd.api+json";
319
+ /** The Atomic Operations extension URI. */
320
+ declare const ATOMIC_EXT = "https://jsonapi.org/ext/atomic";
321
+ /**
322
+ * The media-type parameters a request opts into: extension URIs (`ext`) and/or profile URIs
323
+ * (`profile`). Each is rendered as a space-joined, quoted media-type parameter per RFC 6839 /
324
+ * the JSON:API spec (`ext="<uri> <uri>"`, `profile="<uri> <uri>"`).
325
+ */
326
+ interface MediaTypeParams {
327
+ ext?: readonly string[] | undefined;
328
+ profiles?: readonly string[] | undefined;
329
+ }
330
+ /**
331
+ * Compose the JSON:API media type with any opted-in `ext`/`profile` parameters. Each parameter
332
+ * is a space-joined, double-quoted URI list, in the canonical order `ext` then `profile` so the
333
+ * output is deterministic (stable for caching/assertions). Empty/absent lists are omitted; with
334
+ * neither, the bare {@link JSON_API_MEDIA_TYPE} is returned (no behaviour change for a plain
335
+ * request). Used for both `Accept` and (when a body is present) `Content-Type`.
336
+ */
337
+ declare function mediaType(params?: MediaTypeParams): string;
338
+ /**
339
+ * The media type for an Atomic Operations request/response: the JSON:API media type carrying
340
+ * the atomic `ext` parameter. Sent as both `Content-Type` and `Accept` on a `client.atomic`
341
+ * batch (per the JSON:API atomic extension).
342
+ */
343
+ declare const ATOMIC_MEDIA_TYPE: string;
344
+ /** A resource identifier: the shape a non-included but linked relation takes. */
345
+ interface ResourceIdentifier<TType extends string = string> {
346
+ type: TType;
347
+ id: string;
348
+ meta?: Record<string, unknown>;
349
+ }
350
+ /** A local id reference used inside an atomic transaction (`{ type, lid }`). */
351
+ interface LocalIdentifier<TType extends string = string> {
352
+ type: TType;
353
+ lid: string;
354
+ }
355
+ //#endregion
356
+ //#region src/atomic.d.ts
357
+ /**
358
+ * A reference to a resource for an `update`/`remove` op: a bare identifier (or a materialised
359
+ * resource — its `{type,id}` extracted), OR a `{ type, lid }` local identifier targeting a
360
+ * resource CREATED EARLIER IN THE SAME BATCH (e.g. a `tx.create` handle), so an op can update
361
+ * or remove a just-created resource that has no server id yet (per the JSON:API atomic ext,
362
+ * `ref` may carry `lid` instead of `id`).
363
+ */
364
+ type AtomicRef = ResourceIdentifier | {
365
+ type: string;
366
+ id: string;
367
+ } | LocalIdentifier | {
368
+ type: string;
369
+ lid: string;
370
+ };
371
+ /**
372
+ * Flat create input for type `T`: the `type` discriminant plus the type's flat create
373
+ * attributes/relation slots (reusing {@link CreateInput}; relation slots may reference a prior
374
+ * `tx.create` handle). `D`/`W` thread the descriptor + write-attribute map for precise fields;
375
+ * the loose defaults keep a codegen-less caller compiling.
376
+ */
377
+ type AtomicCreateInput<D extends ApiDescriptor, W, T extends TypeName<D>> = {
378
+ type: T;
379
+ } & CreateInput<D, W, T>;
380
+ /**
381
+ * Flat update input for type `T`: the `type` discriminant + the target's identity (`id`, OR a
382
+ * `lid` for a resource created earlier in the same batch) plus the type's flat update
383
+ * attributes/relation slots (reusing {@link UpdateInput}).
384
+ */
385
+ type AtomicUpdateInput<D extends ApiDescriptor, W, T extends TypeName<D>> = ({
386
+ type: T;
387
+ id: string;
388
+ lid?: never;
389
+ } & UpdateInput<D, W, T>) | ({
390
+ type: T;
391
+ lid: string;
392
+ id?: never;
393
+ } & UpdateInput<D, W, T>);
394
+ /**
395
+ * The handle a {@link AtomicRecorder.create} returns. It is a `lid`-bearing relationship ref
396
+ * (`{ type, lid }`) — pass it (or spread it) into a later op's relation slot to wire the
397
+ * just-created resource without a server id. It also carries its op `kind` (`'create'`) and its
398
+ * `opIndex` (its position in the batch, the created resource's positional result) so the typed
399
+ * result tuple can map it back to the right positional result.
400
+ */
401
+ interface AtomicCreateHandle<T extends string = string> extends LocalIdentifier {
402
+ readonly type: T;
403
+ readonly kind: 'create';
404
+ readonly opIndex: number;
405
+ }
406
+ /**
407
+ * The handle a {@link AtomicRecorder.update} returns: its type discriminant `T`, its op `kind`
408
+ * (`'update'`) and its `opIndex`. Returned (alongside the create handle) from the callback so its
409
+ * positional result is typed as the materialised resource of `T`.
410
+ */
411
+ interface AtomicUpdateHandle<T extends string = string> {
412
+ readonly type: T;
413
+ readonly kind: 'update';
414
+ readonly opIndex: number;
415
+ }
416
+ /**
417
+ * The handle a {@link AtomicRecorder.delete} returns: its op `kind` (`'delete'`) and its
418
+ * `opIndex`. A delete carries no data, so its positional result is `undefined`.
419
+ */
420
+ interface AtomicDeleteHandle {
421
+ readonly kind: 'delete';
422
+ readonly opIndex: number;
423
+ }
424
+ /**
425
+ * Any handle a recorder method returns — the element type of the callback's result tuple, used
426
+ * as the inference constraint for the typed-tuple `atomic` form. The discriminant is left open
427
+ * (`string`, NOT `TypeName<D>`) DELIBERATELY, so the type is descriptor-agnostic: a tighter
428
+ * `TypeName<D>` constraint would flow the full type union into each element's contextual type
429
+ * during array-literal inference, widening a `tx.create({ type: 'albums' })` handle's `T` back to
430
+ * the whole union — losing the per-op narrowing. The recorder methods already guarantee each `T`
431
+ * is a real `TypeName<D>`, so the open constraint is sound; `AtomicResultOf` re-infers the precise
432
+ * `T` per element (guarded by `TypeName<D>` there).
433
+ */
434
+ type AtomicHandle = AtomicCreateHandle<string> | AtomicUpdateHandle<string> | AtomicDeleteHandle;
435
+ /**
436
+ * The recorder handed to the `client.atomic` callback. Each method appends an op (preserving
437
+ * order) and returns a handle carrying the op's discriminant `T`, `kind`, and `opIndex`. The
438
+ * methods are generic over `T` (inferred from the input's `type`), so a create/update is typed
439
+ * by the type's write-input ({@link CreateInput}/{@link UpdateInput}) and its handle resolves to
440
+ * the materialised resource of `T`. `create` still returns a usable `{ type, lid }` ref so a
441
+ * just-created resource wires into a later op without a server id.
442
+ */
443
+ interface AtomicRecorder<D extends ApiDescriptor, W> {
444
+ create<T extends TypeName<D>>(input: AtomicCreateInput<D, W, T>): AtomicCreateHandle<T>;
445
+ update<T extends TypeName<D>>(input: AtomicUpdateInput<D, W, T>): AtomicUpdateHandle<T>;
446
+ delete(ref: AtomicRef): AtomicDeleteHandle;
447
+ }
448
+ /** One positional result of an atomic batch: the materialised `data` (a resource/`null`) plus any op `meta`. */
449
+ interface AtomicResult<Data = unknown> {
450
+ data: Data;
451
+ meta?: Record<string, unknown>;
452
+ }
453
+ /**
454
+ * The typed result tuple for a callback that returns a tuple of handles `Ops`: each returned
455
+ * handle maps (by {@link AtomicResultOf}) to its materialised positional result — a create/update
456
+ * handle to the `AtomicResult` of its type, a delete handle to `undefined`. The mapping is
457
+ * positional in the RETURNED order (sound regardless of subset/reorder, since the runtime resolves
458
+ * each handle by its `opIndex`).
459
+ */
460
+ type AtomicResults<D extends ApiDescriptor, A, Ops extends readonly AtomicHandle[]> = { -readonly [K in keyof Ops]: AtomicResultOf<D, A, Ops[K]> };
461
+ /**
462
+ * Run an atomic transaction. Invokes `build` with a recorder, collecting the operations (always
463
+ * via the recorder's side effect, preserving op order), then posts `{ "atomic:operations": [...] }`
464
+ * to the atomic endpoint with the ext media type as both `Content-Type` and `Accept`. The server's
465
+ * `{ "atomic:results": [...] }` is parsed positionally — each result's `data` materialised the same
466
+ * as a read.
467
+ *
468
+ * The return shape depends on what `build` returns:
469
+ *
470
+ * - returns a tuple of handles -> a per-op POSITIONALLY-TYPED tuple: each returned handle is
471
+ * resolved to its result BY ITS `opIndex` (`results[handle.opIndex]`), so the tuple is sound
472
+ * regardless of the order/subset the callback returns the handles in (a delete handle resolves
473
+ * to `undefined`);
474
+ * - returns void/nothing -> the loose, ordered `AtomicResult[]` (backward-compatible).
475
+ *
476
+ * A thrown {@link JsonApiError} has each error's pointer remapped to `(opIndex, flatPath)` (the
477
+ * failing op's type drives the flat-path inversion) before it propagates.
478
+ */
479
+ declare function runAtomic<D extends ApiDescriptor, A, W, const Ops>(request: JsonApiContext, materialiseCtx: MaterialiseContext, descriptor: D, path: string, build: (tx: AtomicRecorder<D, W>) => Ops): Promise<Ops extends readonly AtomicHandle[] ? AtomicResults<D, A, Ops> : AtomicResult[]>;
480
+ //#endregion
481
+ //#region src/result-types.d.ts
482
+ /**
483
+ * An augmented array (CONTEXT.md "To-many relationship values are augmented arrays"):
484
+ * a real, read-only `T[]` carrying the non-enumerable relationship-level envelope
485
+ * (`$page`/`$links`/`$meta`) plus link-driven `$next()`/`$prev()` navigation. The same
486
+ * model backs top-level collections, related-endpoint collections, and hydrated/linkage
487
+ * to-many relationship values ("one model, three surfaces").
488
+ */
489
+ type Collection<T> = ReadonlyArray<T> & Omit<ArrayEnvelope, '$next' | '$prev'> & {
490
+ $next(): Promise<Collection<T> | undefined>;
491
+ $prev(): Promise<Collection<T> | undefined>;
492
+ };
493
+ /**
494
+ * The edge-local accessors carried by a materialised related VALUE (a per-edge view):
495
+ * `$edge` (this membership's edge meta/links) and `$pivot` (typed sugar over
496
+ * `$edge.meta.pivot`). `$pivot` is graceful: present iff the endpoint rendered
497
+ * `meta.pivot` (relationship/related endpoints), `undefined` otherwise.
498
+ */
499
+ interface EdgeMembers<Pivot = Record<string, unknown>> {
500
+ readonly $edge: Edge | undefined;
501
+ readonly $pivot: Pivot | undefined;
502
+ }
503
+ /** The resource-level `$`-accessors on a materialised resource object. */
504
+ interface ResourceAccessors {
505
+ readonly $meta: Record<string, unknown> | undefined;
506
+ readonly $links: ResourceLinks | undefined;
507
+ readonly $self: string | undefined;
508
+ readonly $document: DocumentEnvelope;
509
+ readonly $raw: Record<string, unknown>;
510
+ $rel(name: string): RelationView | undefined;
511
+ }
512
+ /** Every wire type declared by the descriptor `D`. */
513
+ type TypeName<D extends ApiDescriptor> = Extract<keyof D, string>;
514
+ /** The relation names declared on type `T` of descriptor `D`. */
515
+ type RelationName<D extends ApiDescriptor, T extends TypeName<D>> = Extract<keyof D[T]['relations'], string>;
516
+ /** The attribute object for type `T`, from the generated attributes map `A` (falls back to `{}`). */
517
+ type AttributesOf<A, T> = T extends keyof A ? A[T] : Record<string, never>;
518
+ /**
519
+ * A sparse-fieldset selection map: wire type -> the member names (`fields[type]`) a read
520
+ * requested for that type. Threaded through {@link ResourceObjectView} so an unrequested
521
+ * attribute/relation is statically ABSENT, matching the runtime (the server only emits the
522
+ * requested members). The default `unknown` means "no fieldset narrowing" — every member is
523
+ * present, as it was before.
524
+ */
525
+ type FieldSelectionMap = Readonly<Record<string, readonly string[]>>;
526
+ /**
527
+ * The selected member names for type `T` from a fieldset map `F`, or the {@link AllFields}
528
+ * sentinel when `F` declares no entry for `T` (so that type's members are all present). The
529
+ * `unknown` map (no `fields` query at all) also resolves to {@link AllFields}. A member of `F`
530
+ * keyed `T` whose value is `readonly string[]` selects exactly those names.
531
+ */
532
+ type AllFields = {
533
+ readonly __allFields: unique symbol;
534
+ };
535
+ /** Resolve the selected member-name union for type `T` from fieldset map `F` (or {@link AllFields}). */
536
+ type SelectedFields<F, T extends string> = F extends FieldSelectionMap ? T extends keyof F ? F[T][number] : AllFields : AllFields;
537
+ /**
538
+ * Pick only the members of `Obj` selected by `Sel` (a member-name union, or {@link AllFields}
539
+ * to keep them all). When narrowing, a key absent from `Sel` is dropped, so the resulting
540
+ * object's keys are exactly the requested members — the static mirror of a sparse fieldset.
541
+ */
542
+ type PickSelected<Obj, Sel> = [Sel] extends [AllFields] ? Obj : Pick<Obj, Extract<keyof Obj, Sel>>;
543
+ /**
544
+ * The related wire type(s) of relation `R` on type `T`. A union when polymorphic; the
545
+ * member is narrowed to types that actually exist in the descriptor so the hydrated
546
+ * shape composes.
547
+ */
548
+ type RelatedType<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = Extract<D[T]['relations'][R]['types'][number], TypeName<D>>;
549
+ /** True when relation `R` of type `T` is to-many. */
550
+ type IsMany<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = D[T]['relations'][R]['cardinality'] extends 'many' ? true : false;
551
+ /**
552
+ * The bare resource-identifier shape a non-included (but linked) relation takes,
553
+ * carrying the per-edge `$edge`/`$pivot` accessors the runtime attaches to every
554
+ * materialised related value.
555
+ */
556
+ type IdentifierMember<TType extends string, Pivot = Record<string, unknown>> = ResourceIdentifier<TType> & EdgeMembers<Pivot>;
557
+ /**
558
+ * A hydrated related resource: a per-edge VIEW — the full `ResourceObjectView` of the
559
+ * related node plus this membership's `$edge`/`$pivot`. Relations one hop deep stay as
560
+ * identifiers (no nested-include narrowing in the read API today). The fieldset map `F`
561
+ * threads through so a sparse fieldset on the included type narrows its attributes/relations.
562
+ * `Pivot` types the per-edge `$pivot` (a `belongsToMany` relation's `meta.pivot`).
563
+ */
564
+ type HydratedMember<D extends ApiDescriptor, A, TType extends TypeName<D>, F = unknown, Pivot = Record<string, unknown>> = ResourceObjectView<D, A, TType, never, F> & EdgeMembers<Pivot>;
565
+ /** A hydrated related value distributes over a polymorphic union of related types. */
566
+ type Hydrated<D extends ApiDescriptor, A, TType extends TypeName<D>, F = unknown, Pivot = Record<string, unknown>> = TType extends TypeName<D> ? HydratedMember<D, A, TType, F, Pivot> : never;
567
+ /** An identifier related value distributes over a polymorphic union of related types. */
568
+ type Identifier<TType extends string, Pivot = Record<string, unknown>> = TType extends string ? IdentifierMember<TType, Pivot> : never;
569
+ /**
570
+ * The static value of one relation slot, resolved by cardinality and whether the
571
+ * relation name is present in the read's `include` tuple `Inc`:
572
+ *
573
+ * - to-one, included -> the hydrated related resource, or `null` (an empty to-one with
574
+ * `data: null` materialises to `null` even when included);
575
+ * - to-one, excluded -> `Identifier | null | undefined` (absent/links-only/empty are valid);
576
+ * - to-many, included -> a `Collection` of hydrated members;
577
+ * - to-many, excluded -> a `Collection` of identifier members.
578
+ *
579
+ * (Excluded to-many stays a `Collection`: linkage may still be present, and a lazy
580
+ * to-many materialises as an augmented array regardless — only its members differ.)
581
+ */
582
+ type RelationValue<D extends ApiDescriptor, A, T extends TypeName<D>, R extends RelationName<D, T>, Inc, F = unknown> = R extends Inc ? IsMany<D, T, R> extends true ? Collection<Hydrated<D, A, RelatedType<D, T, R>, F>> : Hydrated<D, A, RelatedType<D, T, R>, F> | null : IsMany<D, T, R> extends true ? Collection<Identifier<RelatedType<D, T, R>>> : Identifier<RelatedType<D, T, R>> | null | undefined;
583
+ /**
584
+ * The relation slots of type `T`, each typed by cardinality and include-presence, then
585
+ * narrowed to the fieldset selection for `T` (relations are sparse-fieldset members too — an
586
+ * unrequested relation is statically absent).
587
+ */
588
+ type Relations<D extends ApiDescriptor, A, T extends TypeName<D>, Inc, F = unknown> = PickSelected<{ [R in RelationName<D, T>]: RelationValue<D, A, T, R, Inc, F> }, SelectedFields<F, T>>;
589
+ /**
590
+ * A materialised resource object: flattened `type` + `id` + attributes + relation slots
591
+ * as own enumerable props, intersected with the resource-level `$`-accessors. `Inc` is
592
+ * the union of relation names included by the read (narrows relations to hydrated); `F` is
593
+ * the sparse-fieldset selection map (narrows attributes/relations to the requested members —
594
+ * `type`/`id`/the `$`-accessors are always present). The default `F = unknown` keeps every
595
+ * member (no fieldset narrowing).
596
+ */
597
+ type ResourceObjectView<D extends ApiDescriptor, A, T extends TypeName<D>, Inc = never, F = unknown> = {
598
+ type: T;
599
+ id: string;
600
+ } & PickSelected<AttributesOf<A, T>, SelectedFields<F, T>> & Relations<D, A, T, Inc, F> & ResourceAccessors;
601
+ /**
602
+ * The element type of the `include` array for type `T`: a path from the descriptor's
603
+ * `includable` enum — the exact set the server advertises, including nested dotted paths
604
+ * (`tracks.album`). `never` when the type advertises nothing includable (so `include` is a
605
+ * compile error, matching the server's `400 INCLUSION_NOT_ALLOWED`). Only the top-level
606
+ * relation of each path drives return-type narrowing (see {@link IncludedRelations}).
607
+ */
608
+ type IncludePath<D extends ApiDescriptor, T extends TypeName<D>> = D[T] extends {
609
+ includable: infer I;
610
+ } ? I extends readonly (infer P)[] ? P & string : never : never;
611
+ /**
612
+ * The union of top-level relation names present in an `include` tuple (drops dotted children).
613
+ * Mapped element-wise over the tuple (`{ [K in keyof Inc]: ... }[number]`) rather than
614
+ * distributed over `Inc[number]` — a distributive template-literal `infer` collapses the union to
615
+ * a single head when the tuple mixes dotted and plain paths (e.g. `['artist', 'tracks.album']`
616
+ * would drop `tracks`), silently leaving an included relation un-hydrated.
617
+ */
618
+ type IncludedRelations<D extends ApiDescriptor, T extends TypeName<D>, Inc extends readonly IncludePath<D, T>[]> = Extract<{ [K in keyof Inc]: Inc[K] extends `${infer Head}.${string}` ? Head : Inc[K] }[number], RelationName<D, T>>;
619
+ /**
620
+ * The `withCount` count tokens type `T` accepts (its `countable.tokens` literal union), or
621
+ * `never` when the type advertises no Countable profile (`withCount` is then unusable). `_self_`
622
+ * counts the collection; a relation name counts that relation per item.
623
+ */
624
+ type CountToken<D extends ApiDescriptor, T extends TypeName<D>> = D[T] extends {
625
+ countable: {
626
+ tokens: infer Tokens;
627
+ };
628
+ } ? Tokens extends readonly (infer Token)[] ? Token & string : never : never;
629
+ /**
630
+ * A sort token type `T`'s COLLECTION read accepts (its `sortable` literal union — signed
631
+ * field names like `title`/`-title`), or `never` when the collection advertises no sorting.
632
+ */
633
+ type SortToken<D extends ApiDescriptor, T extends TypeName<D>> = D[T] extends {
634
+ sortable: infer S;
635
+ } ? S extends readonly (infer Token)[] ? Token & string : never : never;
636
+ /**
637
+ * A filter key type `T`'s COLLECTION read accepts (its `filterable` literal union), or `never`
638
+ * when the collection advertises no filters.
639
+ */
640
+ type FilterKey<D extends ApiDescriptor, T extends TypeName<D>> = D[T] extends {
641
+ filterable: infer Fl;
642
+ } ? Fl extends readonly (infer Key)[] ? Key & string : never : never;
643
+ /**
644
+ * The `sort` value for type `T`: one of the advertised tokens or a tuple of them. `never`
645
+ * (so `sort` cannot be supplied at all) when the collection advertises no sorting — a
646
+ * compile-time mirror of the server's `400 SORTING_UNSUPPORTED`.
647
+ */
648
+ type SortQuery<D extends ApiDescriptor, T extends TypeName<D>> = [SortToken<D, T>] extends [never] ? never : SortToken<D, T> | readonly SortToken<D, T>[];
649
+ /**
650
+ * The `filter` object for type `T`: keys constrained to the advertised filter params (values
651
+ * stay `unknown` — value shapes/operators vary). `never` (so `filter` cannot be supplied) when
652
+ * the collection advertises no filters — a compile-time mirror of `400 FILTERING_UNRECOGNIZED`.
653
+ */
654
+ type FilterQuery<D extends ApiDescriptor, T extends TypeName<D>> = [FilterKey<D, T>] extends [never] ? never : Partial<Record<FilterKey<D, T>, unknown>>;
655
+ /**
656
+ * The element type of the `fields` map for a typed read: a member-name array per wire type,
657
+ * each name constrained to that type's declared attributes/relations. Capturing the literal
658
+ * names (a `const` tuple) drives the sparse-fieldset return-type narrowing.
659
+ */
660
+ type FieldsMap<D extends ApiDescriptor> = { [T in TypeName<D>]?: readonly MemberName<D, T>[] };
661
+ /**
662
+ * A selectable sparse-fieldset member of type `T`: an attribute name (the descriptor's
663
+ * `attributes` keys) or a relation name. The descriptor carries the literal member names, so
664
+ * `fields` entries are constrained to real members.
665
+ */
666
+ type MemberName<D extends ApiDescriptor, T extends TypeName<D>> = Extract<keyof D[T]['attributes'], string> | RelationName<D, T>;
667
+ /**
668
+ * A read query carrying a typed, narrowing `include`, a typed `fields` (drives sparse-fieldset
669
+ * return narrowing), a `withCount` constrained to the type's count tokens, plus the loose
670
+ * JSON:API query families. `Inc`/`F` are inferred (as `const`) at the call site. Used for the
671
+ * COLLECTION read (`list`); the single-resource read (`get`) uses {@link SingleReadQuery}, which
672
+ * drops `withCount` (no `GET /{type}/{id}` endpoint advertises it).
673
+ */
674
+ interface TypedReadQuery<D extends ApiDescriptor, T extends TypeName<D>, Inc extends readonly IncludePath<D, T>[], F extends FieldsMap<D> = FieldsMap<D>> {
675
+ /** The relations to include — drives static narrowing; constrained to the advertised paths. */
676
+ include?: Inc;
677
+ /** Filter the collection — keys constrained to the type's advertised `filter[...]` params. */
678
+ filter?: FilterQuery<D, T>;
679
+ /** Sort the collection — constrained to the type's advertised (signed) sort tokens. */
680
+ sort?: SortQuery<D, T>;
681
+ /** Sparse fieldsets per type — drives return-type narrowing (unrequested members are absent). */
682
+ fields?: F;
683
+ /** Relationship-count tokens (the Countable profile) — comma-joined onto `withCount`. */
684
+ withCount?: readonly CountToken<D, T>[];
685
+ page?: Record<string, unknown>;
686
+ }
687
+ /**
688
+ * A single-resource read query (`GET /{type}/{id}`): only `include` and sparse `fields`. A
689
+ * single-resource endpoint advertises neither `sort`/`filter`/`page` (no collection to order,
690
+ * filter or page) nor `withCount` — accepting any of them would be a lying type (the server
691
+ * rejects an unrecognised parameter with `400` under strict query-param validation).
692
+ */
693
+ interface SingleReadQuery<D extends ApiDescriptor, T extends TypeName<D>, Inc extends readonly IncludePath<D, T>[], F extends FieldsMap<D> = FieldsMap<D>> {
694
+ /** The relations to include — drives static narrowing; constrained to the advertised paths. */
695
+ include?: Inc;
696
+ /** Sparse fieldsets per type — drives return-type narrowing (unrequested members are absent). */
697
+ fields?: F;
698
+ }
699
+ /**
700
+ * The static return type of a read on type `T` given its `include` tuple and `fields` selection
701
+ * `F` — the resource object with the named relations narrowed to hydrated and its members
702
+ * narrowed to the requested sparse fieldset.
703
+ */
704
+ type ReadResult<D extends ApiDescriptor, A, T extends TypeName<D>, Inc extends readonly IncludePath<D, T>[] = [], F = unknown> = ResourceObjectView<D, A, T, IncludedRelations<D, T, Inc>, F>;
705
+ /** A loose read query (filter/sort/include/fields/page/withCount) for the relationship/related endpoints. */
706
+ interface RelationReadQuery {
707
+ filter?: Record<string, unknown>;
708
+ sort?: string | readonly string[];
709
+ include?: readonly string[];
710
+ fields?: Record<string, readonly string[]>;
711
+ page?: Record<string, unknown>;
712
+ /**
713
+ * Relationship-count tokens (`_self_` + the relation's countable relations) — the related/
714
+ * relationship endpoints advertise their own `withCount`, and the client negotiates the
715
+ * relation's Countable profile so a strict server accepts it (D3).
716
+ */
717
+ withCount?: readonly string[];
718
+ }
719
+ /** The TS type a descriptor wire-format hint maps to (mirrors the runtime coercion vocabulary). */
720
+ type FormatToTs<H> = H extends 'number' | 'integer' ? number : H extends 'boolean' ? boolean : H extends 'object' ? Record<string, unknown> : H extends 'array' ? readonly unknown[] : H extends 'unknown' ? unknown : string;
721
+ /**
722
+ * The typed `meta.pivot` shape of relation `R` — built from the descriptor's per-relation
723
+ * `pivotFields` (field-name → format hint), e.g. `{ position: number; weight: number; addedAt:
724
+ * string }`. Falls back to a loose record when the relation carries no pivot fields (D33).
725
+ */
726
+ type PivotFieldsOf<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = D[T]['relations'][R] extends {
727
+ pivotFields: infer P;
728
+ } ? { readonly [K in keyof P]: FormatToTs<P[K]> } : Record<string, unknown>;
729
+ /**
730
+ * A hydrated related VALUE for relation `R` of type `T`, resolved by cardinality over the
731
+ * (possibly polymorphic) related type(s): a `Collection` of hydrated members for to-many,
732
+ * the hydrated member (or `null`) for to-one. Each member's `$pivot` is typed from the
733
+ * relation's `pivotFields` (D33).
734
+ */
735
+ type RelatedValue<D extends ApiDescriptor, A, T extends TypeName<D>, R extends RelationName<D, T>> = IsMany<D, T, R> extends true ? Collection<Hydrated<D, A, RelatedType<D, T, R>, unknown, PivotFieldsOf<D, T, R>>> : Hydrated<D, A, RelatedType<D, T, R>, unknown, PivotFieldsOf<D, T, R>> | null;
736
+ /**
737
+ * A linkage VALUE for relation `R` (the `/relationships/{rel}` endpoint, identifiers only):
738
+ * a `Collection` of identifier members for to-many, a single identifier (or `null`) for to-one.
739
+ * Each member's `$pivot` is typed from the relation's `pivotFields` (D33).
740
+ */
741
+ type LinkageValue<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = IsMany<D, T, R> extends true ? Collection<Identifier<RelatedType<D, T, R>, PivotFieldsOf<D, T, R>>> : Identifier<RelatedType<D, T, R>, PivotFieldsOf<D, T, R>> | null;
742
+ /**
743
+ * A relationship accessor off a {@link ResourceHandle} (`client.albums.id('1').tracks`):
744
+ * reads — `.get()` (linkage) / `.related()` (related collection); writes — to-many
745
+ * `.add`/`.remove`/`.replace([refs])` (POST/DELETE/PATCH) and to-one `.set(ref|null)`
746
+ * (PATCH). A read is present only when the relation exposes that endpoint (the descriptor's
747
+ * `related`/`relationship` flags, from the bundle's `withoutRelatedEndpoint()` /
748
+ * `withoutRelationshipEndpoint()`, ADR 0027); a write is present only when the relationship
749
+ * endpoint advertises that verb (the per-relation `mutations` flags, from `cannotAdd`/
750
+ * `cannotRemove`/`cannotReplace`). A suppressed read or forbidden verb is typed `never`, so
751
+ * calling it is a compile error rather than a server round-trip. A write returns the materialised
752
+ * linkage, or `void` when the server replies `204`.
753
+ */
754
+ interface RelationshipAccessor<D extends ApiDescriptor, A, T extends TypeName<D>, R extends RelationName<D, T>> {
755
+ get: ExposesRelationship<D, T, R> extends true ? (query?: RelationReadQuery) => Promise<LinkageValue<D, T, R>> : never;
756
+ related: ExposesRelated<D, T, R> extends true ? (query?: RelationReadQuery) => Promise<RelatedValue<D, A, T, R>> : never;
757
+ add: RelationMutation<D, T, R, 'add', readonly WriteRef<D, T, R>[]>;
758
+ remove: RelationMutation<D, T, R, 'remove', readonly LinkageRefOf<RelatedType<D, T, R>>[]>;
759
+ replace: RelationMutation<D, T, R, 'replace', readonly WriteRef<D, T, R>[]>;
760
+ set: RelationMutation<D, T, R, 'set', LinkageRefOf<RelatedType<D, T, R>> | null>;
761
+ }
762
+ /**
763
+ * A linkage ref for a to-many membership WRITE (`add`/`replace`) on relation `R`, with `$pivot`
764
+ * typed from the relation's `pivotFields` — so a `belongsToMany` add carries the precise pivot
765
+ * shape (`{ position, weight, … }`) instead of a loose record (D33).
766
+ */
767
+ type WriteRef<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = LinkageRefOf<RelatedType<D, T, R>> & {
768
+ $pivot?: PivotFieldsOf<D, T, R>;
769
+ };
770
+ /**
771
+ * True unless relation `R`'s related endpoint is suppressed (`related: false`, the bundle's
772
+ * `withoutRelatedEndpoint()`). Absent flag ⇒ exposed (the default / a hand-written descriptor).
773
+ */
774
+ type ExposesRelated<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = D[T]['relations'][R] extends {
775
+ related: false;
776
+ } ? false : true;
777
+ /**
778
+ * True unless relation `R`'s relationship endpoint is suppressed (`relationship: false`, the
779
+ * bundle's `withoutRelationshipEndpoint()`). Absent flag ⇒ exposed (the default).
780
+ */
781
+ type ExposesRelationship<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = D[T]['relations'][R] extends {
782
+ relationship: false;
783
+ } ? false : true;
784
+ /** The mutation verb a relationship accessor method maps to (`add`/`remove`/`replace` to-many, `set` to-one). */
785
+ type MutationVerb = 'add' | 'remove' | 'replace' | 'set';
786
+ /** The cardinality a mutation verb belongs to (`set` is to-one; the rest are to-many). */
787
+ type VerbCardinality<V extends MutationVerb> = V extends 'set' ? 'one' : 'many';
788
+ /**
789
+ * True when relation `R` of type `T` advertises mutation verb `V`. The descriptor's
790
+ * `mutations` flags are authoritative when present; when the relation carries no
791
+ * `mutations` block at all (an older/looser descriptor), gating falls back to cardinality
792
+ * alone so the verb stays callable. Crucially, an explicit `mutations: {}` (or one missing
793
+ * the verb's flag) gates the verb OFF.
794
+ */
795
+ type AdvertisesVerb<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>, V extends MutationVerb> = D[T]['relations'][R] extends {
796
+ mutations: infer M;
797
+ } ? M extends Partial<Record<V, boolean>> ? M[V] extends true ? true : false : false : true;
798
+ /**
799
+ * A relationship-mutation method, gated by both cardinality and the per-relation verb
800
+ * capability: present (typed `(refs) => Promise<…>`) only when the relation's cardinality
801
+ * matches the verb's cardinality AND the relation advertises the verb (see
802
+ * {@link AdvertisesVerb}); otherwise `never`, so calling the wrong verb for the cardinality
803
+ * — or a verb the relation forbids (e.g. a to-many lacking `replace`) — is a compile error.
804
+ * The result is the materialised linkage value or `void` (a `204` response).
805
+ */
806
+ type RelationMutation<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>, V extends MutationVerb, Refs> = D[T]['relations'][R]['cardinality'] extends VerbCardinality<V> ? AdvertisesVerb<D, T, R, V> extends true ? (refs: Refs) => Promise<LinkageValue<D, T, R> | void> : never : never;
807
+ /** The relationship accessors of type `T`, keyed by relation name. */
808
+ type RelationshipAccessors<D extends ApiDescriptor, A, T extends TypeName<D>> = { [R in RelationName<D, T>]: RelationshipAccessor<D, A, T, R> };
809
+ /**
810
+ * Writable pivot data carried on a to-many member as `$pivot` (a belongsToMany relation
811
+ * with `pivot: true`). Loose by design — the OpenAPI write attributes don't model per-edge
812
+ * pivot fields, so the runtime passes the object through as `meta.pivot`.
813
+ */
814
+ type PivotInput = Record<string, unknown>;
815
+ /**
816
+ * A single linkage reference accepted by a relationship-write input: a bare resource
817
+ * identifier `{ type, id }`, a materialised resource object (its `type`/`id` are extracted by
818
+ * the runtime), OR a local identifier `{ type, lid }` — an atomic-transaction `tx.create` handle
819
+ * referencing a resource created earlier in the same batch (the runtime serialises the `lid`).
820
+ * A to-many member may additionally carry `$pivot`.
821
+ */
822
+ type LinkageRef<TType extends string> = (ResourceIdentifier<TType> & {
823
+ $pivot?: PivotInput;
824
+ }) | (LocalIdentifier<TType> & {
825
+ $pivot?: PivotInput;
826
+ }) | (ResourceObjectView<ApiDescriptor, unknown, TType> & {
827
+ $pivot?: PivotInput;
828
+ });
829
+ /** A linkage ref distributed over a (possibly polymorphic) union of related types. */
830
+ type LinkageRefOf<TType extends string> = TType extends string ? LinkageRef<TType> : never;
831
+ /**
832
+ * The relationship VALUE accepted in a create/update input slot, by cardinality:
833
+ * to-one accepts a single ref or `null` (clear); to-many accepts an array of refs.
834
+ */
835
+ type RelationInput<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = IsMany<D, T, R> extends true ? readonly LinkageRefOf<RelatedType<D, T, R>>[] : LinkageRefOf<RelatedType<D, T, R>> | null;
836
+ /** The relationship slots of type `T` as optional write input keys (one per declared relation). */
837
+ type RelationInputs<D extends ApiDescriptor, T extends TypeName<D>> = { [R in RelationName<D, T>]?: RelationInput<D, T, R> };
838
+ /** The `{ create; update }` write-attribute pair for type `T` (or an open pair when `T` is absent from `W`). */
839
+ type WritePair<W, T> = T extends keyof W ? W[T] : {
840
+ create: Record<string, unknown>;
841
+ update: Record<string, unknown>;
842
+ };
843
+ /** The create attribute object for type `T` from the write map `W`. */
844
+ type CreateAttributesOf<W, T> = WritePair<W, T> extends {
845
+ create: infer C;
846
+ } ? C : object;
847
+ /** The update attribute object for type `T` from the write map `W`. */
848
+ type UpdateAttributesOf<W, T> = WritePair<W, T> extends {
849
+ update: infer U;
850
+ } ? U : object;
851
+ /**
852
+ * The `id` field of a create input, typed by the type's `clientId` policy:
853
+ * `forbidden` -> no `id` key; `optional` -> `id?: string`; `required` -> `id: string`.
854
+ */
855
+ type CreateId<D extends ApiDescriptor, T extends TypeName<D>> = D[T]['clientId'] extends 'required' ? {
856
+ id: string;
857
+ } : D[T]['clientId'] extends 'optional' ? {
858
+ id?: string;
859
+ } : {
860
+ id?: never;
861
+ };
862
+ /**
863
+ * The flat input to `client.<type>.create(input)`: the type's create attributes + relation
864
+ * slots, plus an `id` keyed by the client-id policy. The runtime builds the JSON:API
865
+ * envelope (routing keys to attributes/relationships via the descriptor).
866
+ */
867
+ type CreateInput<D extends ApiDescriptor, W, T extends TypeName<D>> = CreateAttributesOf<W, T> & RelationInputs<D, T> & CreateId<D, T>;
868
+ /**
869
+ * The flat input to `client.<type>.id(id).update(patch)`: the type's update attributes (all
870
+ * optional) + relation slots. The id rides the handle, never the input.
871
+ */
872
+ type UpdateInput<D extends ApiDescriptor, W, T extends TypeName<D>> = UpdateAttributesOf<W, T> & RelationInputs<D, T>;
873
+ /** Options for a create/update write — an optional `include`/`fields` narrowing the materialised response. */
874
+ interface WriteOptions<D extends ApiDescriptor, T extends TypeName<D>, Inc extends readonly IncludePath<D, T>[] = [], F extends FieldsMap<D> = FieldsMap<D>> {
875
+ include?: Inc;
876
+ /** Sparse fieldsets per type — narrows the materialised write response. */
877
+ fields?: F;
878
+ }
879
+ /**
880
+ * The materialised positional result of a single atomic op handle:
881
+ *
882
+ * - an {@link AtomicCreateHandle}/{@link AtomicUpdateHandle} for type `T` -> the `AtomicResult`
883
+ * wrapping the materialised resource view of `T` (the same {@link ResourceObjectView} a
884
+ * standalone create/update read returns — `data` is typed, not `unknown`, plus the optional op
885
+ * `meta`);
886
+ * - an {@link AtomicDeleteHandle} -> `undefined` (a remove carries no data).
887
+ *
888
+ * Drives the per-op positional typing of {@link AtomicResults} (the callback's returned tuple of
889
+ * handles maps to a tuple of these).
890
+ */
891
+ type AtomicResultOf<D extends ApiDescriptor, A, Handle> = Handle extends AtomicDeleteHandle ? undefined : Handle extends AtomicCreateHandle<infer T> ? T extends TypeName<D> ? AtomicResult<ResourceObjectView<D, A, T>> : never : Handle extends AtomicUpdateHandle<infer T> ? T extends TypeName<D> ? AtomicResult<ResourceObjectView<D, A, T>> : never : never;
892
+ /** The custom actions declared on type `T` (the descriptor's `actions` block, or `{}`). */
893
+ type ActionsOf<D extends ApiDescriptor, T extends TypeName<D>> = D[T] extends {
894
+ actions: infer Acts;
895
+ } ? Acts : Record<string, never>;
896
+ /** The action names of type `T` declared at scope `Scope` (collection vs resource). */
897
+ type ActionNameAtScope<D extends ApiDescriptor, T extends TypeName<D>, Scope extends string> = Extract<{ [N in Extract<keyof ActionsOf<D, T>, string>]: ActionsOf<D, T>[N] extends {
898
+ scope: Scope;
899
+ } ? N : never }[Extract<keyof ActionsOf<D, T>, string>], string>;
900
+ /**
901
+ * The generated per-action body/result types (CONTEXT.md "Write surface" — a typed action
902
+ * surface). The codegen emits precise `<Type><Action>Input`/`Output` aliases (expanding the
903
+ * referenced JSON:API component) and threads them in keyed `type -> actionName -> { input?;
904
+ * output? }` as the client's fourth type argument. Default `{}` (no codegen, or `createClient`
905
+ * called directly): actions fall back to the loose `Record<string,unknown>` in / `unknown` out.
906
+ * Deliberately an EMPTY object (no index signature) — an index-signatured map would resolve
907
+ * every action's body type to `never`.
908
+ */
909
+ type DefaultActionTypes = Record<never, never>;
910
+ /** The `{ input?; output? }` body-type entry for action `N` of type `T` from the action map `Act` (empty `{}` when absent). */
911
+ type ActionTypesFor<Act, T extends string, N extends string> = T extends keyof Act ? N extends keyof Act[T] ? Act[T][N] : Record<never, never> : Record<never, never>;
912
+ /**
913
+ * The typed body of a `document` action — the generated input type when the map carries one,
914
+ * else a loose JSON:API document. Gated on the literal `'input'` key being present (not
915
+ * structural `infer`) so a missing entry falls back rather than collapsing to `never`.
916
+ */
917
+ type ActionInputBody<E> = 'input' extends keyof E ? E['input'] : Record<string, unknown>;
918
+ /** The materialised result of a `document` action — the generated output type when present, else `unknown`. */
919
+ type ActionOutputBody<E> = 'output' extends keyof E ? E['output'] : unknown;
920
+ /**
921
+ * The static value an action returns, by its declared `output`:
922
+ *
923
+ * - `document` — the MATERIALISED resource view of the action's `outputType` (matching the read
924
+ * path — `result.title`, not `result.data.attributes.title`), a {@link Collection} when the
925
+ * output is a collection (`outputCardinality: 'many'`). Falls back to the generated
926
+ * {@link ActionOutputBody} only when the descriptor names no resolvable `outputType` (a bespoke
927
+ * document, or a pre-`outputType` generated client);
928
+ * - `meta` — the response document's top-level `meta` object;
929
+ * - `none` — `void` (a `204`).
930
+ */
931
+ type ActionResult<D extends ApiDescriptor, A, Act extends ActionDescriptor, E> = Act extends {
932
+ output: 'document';
933
+ } ? Act extends {
934
+ outputType: infer OT extends TypeName<D>;
935
+ } ? Act extends {
936
+ outputCardinality: 'many';
937
+ } ? Promise<Collection<ReadResult<D, A, OT>>> : Promise<ReadResult<D, A, OT>> : Promise<ActionOutputBody<E>> : Act extends {
938
+ output: 'meta';
939
+ } ? Promise<Record<string, unknown>> : Promise<void>;
940
+ /**
941
+ * A single custom-action method, typed by its declared `input` mode:
942
+ *
943
+ * - `none` — no argument;
944
+ * - `document` — FLAT input (the `inputType`'s {@link CreateInput}, so the client builds the
945
+ * envelope + remaps `422` pointers like `create`); falls back to the generated
946
+ * {@link ActionInputBody} for a bespoke command document with no resolvable `inputType`;
947
+ * - `raw` — an arbitrary body (sent with a relaxed content type).
948
+ *
949
+ * The result is shaped by the action's `output` (see {@link ActionResult}).
950
+ */
951
+ type ActionMethod<D extends ApiDescriptor, A, W, Act extends ActionDescriptor, E> = Act extends {
952
+ input: 'none';
953
+ } ? () => ActionResult<D, A, Act, E> : Act extends {
954
+ input: 'document';
955
+ } ? Act extends {
956
+ inputType: infer IT extends TypeName<D>;
957
+ } ? (input: CreateInput<D, W, IT>) => ActionResult<D, A, Act, E> : (input: ActionInputBody<E>) => ActionResult<D, A, Act, E> : (input: unknown) => ActionResult<D, A, Act, E>;
958
+ /**
959
+ * The typed map of a type's custom actions at one scope (`collection` on the
960
+ * {@link TypeAccessor}, `resource` on the {@link ResourceHandle}). Each entry is an
961
+ * {@link ActionMethod} keyed by the action name, typed from the descriptor (`input`/`output` modes,
962
+ * `inputType`/`outputType`/`outputCardinality`) with `A`/`W` for the materialised result + flat
963
+ * input; only actions declared at `Scope` are present. Empty (`{}`) when the type declares no
964
+ * actions at that scope. `Act` supplies the generated per-action body-type fallbacks.
965
+ */
966
+ type ActionsAccessor<D extends ApiDescriptor, A, W, T extends TypeName<D>, Scope extends string, Act = DefaultActionTypes> = { [N in ActionNameAtScope<D, T, Scope>]: ActionsOf<D, T>[N] extends ActionDescriptor ? ActionMethod<D, A, W, ActionsOf<D, T>[N], ActionTypesFor<Act, T, N>> : never };
967
+ /**
968
+ * A resource handle (no fetch) for a known id. Carries the read surface (`get`), the writes
969
+ * scoped to a known id (`update`/`delete`), a relationship accessor per declared relation (by
970
+ * name), a universal `.rel(name)` fallback (for relations whose name collides with a reserved
971
+ * member like `get`/`update`/`rel`), and `.actions` (the type's resource-scoped custom
972
+ * actions). `W` is the generated write-attribute map.
973
+ */
974
+ type ResourceHandle<D extends ApiDescriptor, A, W, T extends TypeName<D>, Act = DefaultActionTypes> = {
975
+ readonly type: T;
976
+ readonly id: string;
977
+ get<const Inc extends readonly IncludePath<D, T>[] = [], const F extends FieldsMap<D> = {}>(query?: SingleReadQuery<D, T, Inc, F>): Promise<ReadResult<D, A, T, Inc, F>>;
978
+ update<const Inc extends readonly IncludePath<D, T>[] = [], const F extends FieldsMap<D> = {}>(patch: UpdateInput<D, W, T>, opts?: WriteOptions<D, T, Inc, F>): Promise<ReadResult<D, A, T, Inc, F>>;
979
+ delete(): Promise<void>;
980
+ rel<R extends RelationName<D, T>>(name: R): RelationshipAccessor<D, A, T, R>; /** The type's resource-scoped custom actions, keyed by name (sub `{id}` from this handle). */
981
+ readonly actions: ActionsAccessor<D, A, W, T, 'resource', Act>;
982
+ } & RelationshipAccessors<D, A, T>;
983
+ /** The collection-scoped accessor for one wire type (`client.albums`). */
984
+ interface TypeAccessor<D extends ApiDescriptor, A, W, T extends TypeName<D>, Act = DefaultActionTypes> {
985
+ list<const Inc extends readonly IncludePath<D, T>[] = [], const F extends FieldsMap<D> = {}>(query?: TypedReadQuery<D, T, Inc, F>): Promise<Collection<ReadResult<D, A, T, Inc, F>>>;
986
+ get<const Inc extends readonly IncludePath<D, T>[] = [], const F extends FieldsMap<D> = {}>(id: string, query?: SingleReadQuery<D, T, Inc, F>): Promise<ReadResult<D, A, T, Inc, F>>;
987
+ create<const Inc extends readonly IncludePath<D, T>[] = [], const F extends FieldsMap<D> = {}>(input: CreateInput<D, W, T>, opts?: WriteOptions<D, T, Inc, F>): Promise<ReadResult<D, A, T, Inc, F>>;
988
+ id(id: string): ResourceHandle<D, A, W, T, Act>;
989
+ /** The type's collection-scoped custom actions, keyed by name. */
990
+ readonly actions: ActionsAccessor<D, A, W, T, 'collection', Act>;
991
+ }
992
+ /**
993
+ * The descriptor-driven client surface: one {@link TypeAccessor} per wire type. `W` is the
994
+ * generated `WriteAttributes` map (per-type `{ create; update }` attribute pairs) and `Act`
995
+ * the generated per-action body-type map; both thread through to the write/action surfaces and
996
+ * default so read-only / codegen-less callers compile.
997
+ */
998
+ type Client<D extends ApiDescriptor, A = DefaultAttributes<D>, W = DefaultWriteAttributes<D>, Act = DefaultActionTypes> = { [T in TypeName<D>]: TypeAccessor<D, A, W, T, Act> } & {
999
+ /**
1000
+ * Run an Atomic Operations batch (CONTEXT.md "Atomic — typed transaction builder"): the
1001
+ * callback records `create`/`update`/`delete` ops on a non-type-scoped recorder (each carries
1002
+ * its `type`), posted all-or-nothing to the server's atomic endpoint. Present only when the
1003
+ * client was built with the server's `atomic` capability; calling it on an API without one
1004
+ * throws.
1005
+ *
1006
+ * The return shape is driven by what the callback returns (a single signature, branched in the
1007
+ * return type — NOT an overload set, which would defeat the per-op `const` tuple inference):
1008
+ *
1009
+ * - return a tuple of handles -> a PER-OP POSITIONALLY-TYPED tuple of results: a create/update
1010
+ * handle resolves to the `AtomicResult` of that type (data typed as the materialised
1011
+ * resource), a delete handle to `undefined`. The mapping is by `opIndex`, so it stays sound
1012
+ * regardless of the order/subset of handles returned;
1013
+ * - return void/nothing -> the loose, ordered `AtomicResult[]` (backward-compatible).
1014
+ */
1015
+ atomic<const Ops>(build: (tx: AtomicRecorder<D, W>) => Ops): Promise<Ops extends readonly AtomicHandle[] ? AtomicResults<D, A, Ops> : AtomicResult[]>;
1016
+ };
1017
+ /**
1018
+ * The fallback attribute map when a caller doesn't supply the generated `Attributes`
1019
+ * (direct `createClient` users without codegen): every type's attributes are an open
1020
+ * `Record<string, unknown>` so reads still type-check, just without precise fields.
1021
+ */
1022
+ type DefaultAttributes<D extends ApiDescriptor> = { [T in TypeName<D>]: Record<string, unknown> };
1023
+ /**
1024
+ * The fallback write-attribute map when a caller doesn't supply the generated
1025
+ * `WriteAttributes`: every type accepts an open `{ create; update }` pair so writes still
1026
+ * type-check without precise per-type field constraints.
1027
+ */
1028
+ type DefaultWriteAttributes<D extends ApiDescriptor> = { [T in TypeName<D>]: {
1029
+ create: Record<string, unknown>;
1030
+ update: Record<string, unknown>;
1031
+ } };
1032
+ //#endregion
1033
+ //#region src/transport.d.ts
1034
+ /** A single outbound request, already serialised to strings. */
1035
+ interface TransportRequest {
1036
+ method: string;
1037
+ url: string;
1038
+ headers: Record<string, string>;
1039
+ body?: string;
1040
+ }
1041
+ /** A single inbound response, body still a string for the runtime to parse. */
1042
+ interface TransportResponse {
1043
+ status: number;
1044
+ headers: Record<string, string>;
1045
+ body: string;
1046
+ }
1047
+ /**
1048
+ * The transport seam: a tiny, `fetch`-shaped function (not a class), so any impl
1049
+ * (native `fetch`, undici, an axios adapter) drops in. Retries are deliberately out
1050
+ * of scope here — the transport (or the TanStack layer) owns them.
1051
+ */
1052
+ type JsonApiTransport = (req: TransportRequest) => Promise<TransportResponse>;
1053
+ /** The default transport, backed by the global `fetch`. */
1054
+ declare const fetchTransport: JsonApiTransport;
1055
+ //#endregion
1056
+ //#region src/client.d.ts
1057
+ /**
1058
+ * The `fetch` headers init type. `@types/node` declares the global `RequestInit` (used
1059
+ * by the transport) but not `HeadersInit`; we derive it from `RequestInit.headers` to
1060
+ * stay dep-free and consistent with the rest of the runtime's web-globals usage.
1061
+ */
1062
+ type HeadersInit = NonNullable<RequestInit['headers']>;
1063
+ /** Options for {@link createClient}. */
1064
+ interface ClientOptions {
1065
+ /** Base URL the generated path templates are resolved against. */
1066
+ baseUrl: string;
1067
+ /** Transport seam; defaults to the `fetch` adapter when omitted. */
1068
+ transport?: JsonApiTransport;
1069
+ /** Per-request header provider (e.g. bearer auth); may be async. */
1070
+ headers?: () => HeadersInit | Promise<HeadersInit>;
1071
+ /**
1072
+ * The server-level Atomic Operations capability (the codegen's `atomic` constant): `{ path }`
1073
+ * when the server exposes the atomic endpoint, else `null`/omitted. When present, the client
1074
+ * exposes `client.atomic(tx => …)`; otherwise calling it throws.
1075
+ */
1076
+ atomic?: AtomicDescriptor | null;
1077
+ /**
1078
+ * The opt-in per-field validation seam (ADR 0004). Off by default — the client always runs
1079
+ * light structural guards (is this a JSON:API document? does each `data`/`included` member
1080
+ * carry `type`+`id`?) but otherwise trusts the wire. Supply either the schema-driven config
1081
+ * `{ schemas, validator }` — the codegen-emitted per-type `schemas` map plus a validation
1082
+ * engine adapter (each wire resource is validated against `schemas[type]`; a type with no
1083
+ * schema is skipped) — or a bare validator function that owns schema lookup itself. The
1084
+ * validation engine (e.g. ajv) is brought by the caller; the client never depends on one.
1085
+ */
1086
+ validate?: ValidationOption;
1087
+ }
1088
+ /**
1089
+ * The reserved members on a {@link ResourceHandle} — names the Proxy resolves itself, so a
1090
+ * relation of the same name is shadowed and must route through `.rel(name)`. The codegen
1091
+ * imports this to warn at build time (keeping the runtime + the collision detector in sync).
1092
+ * `then` is reserved so a handle is never mistaken for a thenable.
1093
+ */
1094
+ declare const HANDLE_RESERVED: ReadonlySet<string>;
1095
+ /**
1096
+ * Build a descriptor-driven read client. Generic at runtime (a Proxy over the descriptor),
1097
+ * not per-type codegen: `client.<type>` yields a {@link TypeAccessor}, `.id(id)` a handle,
1098
+ * and a handle's relation accessors drive the relationship/related endpoints. Navigation
1099
+ * (`$next()`/`$prev()`, related links) is wired through `materialise`'s `navigate` seam,
1100
+ * which executes the (absolute) link and re-materialises.
1101
+ */
1102
+ declare function createClient<D extends ApiDescriptor, A = DefaultAttributes<D>, W = DefaultWriteAttributes<D>, Act = DefaultActionTypes>(descriptor: D, options: ClientOptions): Client<D, A, W, Act>;
1103
+ //#endregion
1104
+ //#region src/request.d.ts
1105
+ /**
1106
+ * A read query, in the flat shape the fluent surface accepts. Serialised to the
1107
+ * JSON:API bracketed query-parameter families by {@link serializeQuery}.
1108
+ */
1109
+ interface ReadQuery {
1110
+ filter?: Record<string, unknown>;
1111
+ sort?: string | string[];
1112
+ include?: string[];
1113
+ fields?: Record<string, string[]>;
1114
+ /** Relationship-count tokens (the Countable profile) — serialised comma-joined onto `withCount`. */
1115
+ withCount?: readonly string[];
1116
+ page?: Record<string, unknown>;
1117
+ }
1118
+ /** A loose JSON:API document — enough structure for the runtime to materialise. */
1119
+ interface Document {
1120
+ data?: unknown;
1121
+ included?: unknown[];
1122
+ links?: Record<string, unknown>;
1123
+ meta?: Record<string, unknown>;
1124
+ jsonapi?: Record<string, unknown>;
1125
+ }
1126
+ /** The ambient context a request executes in (carried by the client). */
1127
+ interface JsonApiContext {
1128
+ baseUrl: string;
1129
+ transport: JsonApiTransport;
1130
+ headers?: () => HeadersInit | Promise<HeadersInit>;
1131
+ }
1132
+ /** A single read/write request, before serialisation. */
1133
+ interface JsonApiRequest {
1134
+ method: string;
1135
+ /** A path relative to `baseUrl`, or an absolute `http(s)://` URL (e.g. a page link). */
1136
+ path: string;
1137
+ query?: ReadQuery;
1138
+ body?: unknown;
1139
+ /**
1140
+ * Override the request `Content-Type` (defaults to the negotiated JSON:API media type when a
1141
+ * body is present). Set for a custom action's `raw` input, where the payload is not a JSON:API
1142
+ * document. When set it wins over {@link ext}/{@link profiles} for `Content-Type`.
1143
+ */
1144
+ contentType?: string;
1145
+ /**
1146
+ * Override the request `Accept` header (defaults to the negotiated JSON:API media type). Set
1147
+ * for a `raw`-input action that still expects a document response; when set it wins over
1148
+ * {@link ext}/{@link profiles} for `Accept`.
1149
+ */
1150
+ accept?: string;
1151
+ /**
1152
+ * Extension URIs to negotiate (the media-type `ext` parameter), e.g. the atomic ext. Composed
1153
+ * into both `Accept` and (when a body is present) `Content-Type` via the JSON:API media type,
1154
+ * unless {@link accept}/{@link contentType} override the respective header.
1155
+ */
1156
+ ext?: readonly string[];
1157
+ /**
1158
+ * Profile URIs to negotiate (the media-type `profile` parameter), e.g. the Countable profile a
1159
+ * `withCount` read requires. Composed into both `Accept` and (when a body is present)
1160
+ * `Content-Type`, unless {@link accept}/{@link contentType} override the respective header.
1161
+ */
1162
+ profiles?: readonly string[];
1163
+ /**
1164
+ * Send the body verbatim (a `string`) instead of JSON-stringifying it. Used for a `raw`
1165
+ * action body that's already serialised; a non-string body still falls back to
1166
+ * `JSON.stringify`.
1167
+ */
1168
+ raw?: boolean;
1169
+ }
1170
+ /**
1171
+ * Serialise a read query to a query string (no leading `?`). Bracketed keys
1172
+ * (`filter[title]`, `page[number]`, `fields[albums]`) are kept literal; only values
1173
+ * are percent-encoded. Empty/undefined entries are skipped; key order is deterministic
1174
+ * (family order, then insertion order within a family) so URLs are stable for caching.
1175
+ */
1176
+ declare function serializeQuery(query: ReadQuery): string;
1177
+ /**
1178
+ * Execute a request: build the URL (verbatim when already absolute — for page links —
1179
+ * else `baseUrl + path`), apply content negotiation and any async per-request headers,
1180
+ * then drive the transport. A 2xx with a body parses to a {@link Document}; an empty /
1181
+ * 204 body resolves to `undefined`. A non-2xx throws a {@link JsonApiError} carrying the
1182
+ * parsed error objects (tolerating a non-JSON error body with an empty list).
1183
+ */
1184
+ declare function execute(ctx: JsonApiContext, req: JsonApiRequest): Promise<Document | undefined>;
1185
+ //#endregion
1186
+ //#region src/validate.d.ts
1187
+ /**
1188
+ * A wire resource as it appears in a document's `data`/`included` — a JSON:API resource
1189
+ * object (or bare identifier). The structural guards assert `type` (and, where required,
1190
+ * `id`); the opt-in validator sees the whole object to check the rest against its schema.
1191
+ */
1192
+ interface WireResource {
1193
+ type: string;
1194
+ id?: string;
1195
+ [key: string]: unknown;
1196
+ }
1197
+ /**
1198
+ * The opt-in per-resource validator: validate one wire `resource` against its JSON `schema`
1199
+ * (the bundle's per-type resource-object schema). The engine is the user's — throw (or
1200
+ * aggregate-then-throw) on an invalid resource; return for a valid one. With the bare-function
1201
+ * seam form there is no per-type schema, so `schema` is `undefined` and the function owns the
1202
+ * whole decision.
1203
+ */
1204
+ type Validator = (resource: WireResource, schema: unknown) => void;
1205
+ /** A per-type JSON Schema map (the codegen-emitted `schemas` constant), keyed by JSON:API type. */
1206
+ type SchemaMap = Readonly<Record<string, unknown>>;
1207
+ /**
1208
+ * The schema-driven validation seam: a per-type `schemas` map plus a `validator` engine. Each
1209
+ * wire resource is validated against `schemas[resource.type]`; a type with no schema is skipped.
1210
+ */
1211
+ interface ValidationConfig {
1212
+ schemas: SchemaMap;
1213
+ validator: Validator;
1214
+ }
1215
+ /**
1216
+ * The `validate?` option on {@link ClientOptions}: either the schema-driven {@link ValidationConfig}
1217
+ * (the common case — the codegen-emitted `schemas` map + an engine adapter) or a bare
1218
+ * {@link Validator} that owns schema lookup itself (it is called per resource with `schema` =
1219
+ * `undefined`). Absent => no per-field validation (zero overhead, no engine dependency).
1220
+ */
1221
+ type ValidationOption = ValidationConfig | Validator;
1222
+ /** A normalised validator: resolve a resource's schema (or `undefined`) and validate it. */
1223
+ interface ResolvedValidator {
1224
+ validate(resource: WireResource): void;
1225
+ }
1226
+ /**
1227
+ * Normalise the `validate?` option to a single per-resource validator. The config form looks the
1228
+ * schema up by `resource.type` and skips a type with no schema (the seam stays graceful for a
1229
+ * partially-covered bundle); the bare-function form calls the validator with `schema = undefined`,
1230
+ * delegating the whole decision to the caller. Returns `undefined` when validation is off.
1231
+ */
1232
+ declare function resolveValidator(option: ValidationOption | undefined): ResolvedValidator | undefined;
1233
+ /**
1234
+ * The light structural guards every parsed response passes through (ADR 0004): assert the body
1235
+ * is a JSON:API document and that each `data`/`included` member carries a string `type` (and,
1236
+ * for a full resource, an `id`). This is the always-on, engine-free floor — it never duplicates
1237
+ * the opt-in per-field validation; it only proves the envelope invariant the runtime relies on.
1238
+ *
1239
+ * A primary-`data` resource and every `included` resource must carry `type`+`id`. A relationship
1240
+ * endpoint's primary data is pure resource-identifier linkage, which also carries `type`+`id`, so
1241
+ * the same guard holds; `linkage` is accepted for symmetry with the materialiser but the assertion
1242
+ * is identical (an identifier is `type`+`id`). `data: null` (an empty to-one) and a missing
1243
+ * `data` (a meta-only document) are valid and pass.
1244
+ */
1245
+ declare function assertJsonApiDocument(body: unknown): asserts body is Document;
1246
+ /**
1247
+ * Run the opt-in validator over every wire resource in a document's `data` + `included`, each
1248
+ * against its per-type schema. Called only when a validator is configured (off by default). The
1249
+ * structural guards have already proven each member carries `type`+`id`, so the cast is sound.
1250
+ */
1251
+ declare function validateDocument(body: Document, validator: ResolvedValidator): void;
1252
+ //#endregion
1253
+ export { SortQuery as $, DefaultActionTypes as A, RelationView as At, IncludePath as B, AtomicDescriptor as Bt, fetchTransport as C, mediaType as Ct, Collection as D, EdgeEnvelope as Dt, Client as E, Edge as Et, FieldsMap as F, ActionDescriptor as Ft, ReadResult as G, RelationDescriptor as Gt, LinkageRef as H, ClientIdPolicy as Ht, FilterKey as I, ActionInput as It, RelationshipAccessor as J, RelationName as K, RelationMutations as Kt, FilterQuery as L, ActionOutput as Lt, DefaultWriteAttributes as M, ResourceLinks as Mt, EdgeMembers as N, materialise as Nt, CountToken as O, MaterialiseContext as Ot, FieldSelectionMap as P, relatedPaginatorKind as Pt, SingleReadQuery as Q, HydratedMember as R, ActionScope as Rt, TransportResponse as S, ResourceIdentifier as St, AtomicResultOf as T, DocumentEnvelope as Tt, MemberName as U, CountableDescriptor as Ut, IncludedRelations as V, Cardinality as Vt, PivotInput as W, PaginatorKind as Wt, ResourceHandle as X, ResourceAccessors as Y, ResourceObjectView as Z, HANDLE_RESERVED as _, ATOMIC_EXT as _t, Validator as a, WriteOptions as at, JsonApiTransport as b, LocalIdentifier as bt, resolveValidator as c, AtomicDeleteHandle as ct, JsonApiContext as d, AtomicRef as dt, SortToken as et, JsonApiRequest as f, AtomicResult as ft, ClientOptions as g, runAtomic as gt, serializeQuery as h, AtomicUpdateInput as ht, ValidationOption as i, UpdateInput as it, DefaultAttributes as j, ResourceEnvelope as jt, CreateInput as k, Page as kt, validateDocument as l, AtomicHandle as lt, execute as m, AtomicUpdateHandle as mt, SchemaMap as n, TypeName as nt, WireResource as o, AtomicCreateHandle as ot, ReadQuery as p, AtomicResults as pt, RelationReadQuery as q, ResourceDescriptor as qt, ValidationConfig as r, TypedReadQuery as rt, assertJsonApiDocument as s, AtomicCreateInput as st, ResolvedValidator as t, TypeAccessor as tt, Document as u, AtomicRecorder as ut, HeadersInit as v, ATOMIC_MEDIA_TYPE as vt, ActionsAccessor as w, ArrayEnvelope as wt, TransportRequest as x, MediaTypeParams as xt, createClient as y, JSON_API_MEDIA_TYPE as yt, IdentifierMember as z, ApiDescriptor as zt };
1254
+ //# sourceMappingURL=validate-BmyPbJCK.d.cts.map