@haddowg/json-api-client 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,21 @@ interface RelationMutations {
24
24
  replace?: boolean;
25
25
  set?: boolean;
26
26
  }
27
+ /**
28
+ * The pivot fields one side of the wire carries on a member's `meta.pivot`: field name to
29
+ * wire-format hint (the vocabulary of {@link ResourceDescriptor.attributes}), the fields that
30
+ * are always present, and the fields that may be `null`.
31
+ */
32
+ interface PivotDescriptor {
33
+ fields: Readonly<Record<string, string>>;
34
+ /**
35
+ * On a response, the fields every member renders; on a request, the fields every member must
36
+ * send. Absent means every field.
37
+ */
38
+ required?: readonly string[];
39
+ /** The fields whose value may be `null`. Absent means none. */
40
+ nullable?: readonly string[];
41
+ }
27
42
  interface RelationDescriptor {
28
43
  cardinality: Cardinality;
29
44
  /** Related JSON:API type(s); more than one = polymorphic. */
@@ -31,12 +46,30 @@ interface RelationDescriptor {
31
46
  /** Whether members carry pivot data (`meta.pivot`). */
32
47
  pivot?: boolean;
33
48
  /**
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.
49
+ * The pivot fields carried on each member's `meta.pivot`, as a field-name → wire-format-hint
50
+ * map, typed as always present and never `null` on both reads and writes (D33).
51
+ *
52
+ * @deprecated Describe each side with {@link readPivot} and {@link writePivot}. Read only when
53
+ * those are absent.
38
54
  */
39
55
  pivotFields?: Readonly<Record<string, string>>;
56
+ /** The `meta.pivot` a response renders on each member (`<Type><Rel>Pivot`). */
57
+ readPivot?: PivotDescriptor;
58
+ /**
59
+ * The `meta.pivot` a request may send on each member (`<Type><Rel>WritePivot`): a create,
60
+ * update or atomic body, or a relationship mutation.
61
+ */
62
+ writePivot?: PivotDescriptor;
63
+ /**
64
+ * `true` for a relation create and update accept but no response renders. It stays settable
65
+ * on write inputs and is left out of the read view, `fields` and the relationship reads.
66
+ */
67
+ writeOnly?: boolean;
68
+ /**
69
+ * The relation's segment in its related and relationship paths, when it differs from the
70
+ * relation's name (`/articles/{id}/published-by` for `publisher`). Absent means the name.
71
+ */
72
+ uriField?: string;
40
73
  /**
41
74
  * `false` when the related endpoint (`GET /{type}/{id}/{rel}`) is suppressed for this relation
42
75
  * (the bundle's `withoutRelatedEndpoint()`, ADR 0027) — `.related()` is then a compile error /
@@ -64,7 +97,7 @@ interface RelationDescriptor {
64
97
  */
65
98
  countable?: CountableDescriptor;
66
99
  /**
67
- * The paginator kind this relation's OWN related/relationship endpoints advertise
100
+ * The paginator this relation's OWN related/relationship endpoints advertise
68
101
  * (`GET /{type}/{id}/{rel}` / `.../relationships/{rel}`), when it DIVERGES from the related
69
102
  * type's collection-level {@link ResourceDescriptor.paginator}. A related endpoint may paginate
70
103
  * differently than the related type's own collection (e.g. a cursor-paginated related to-many
@@ -72,9 +105,37 @@ interface RelationDescriptor {
72
105
  * related/embedded to-many array's `$page.kind` from this per-relation value first, falling back
73
106
  * to the related type's collection paginator (D6a). Absent when the two agree (the common case).
74
107
  */
75
- paginator?: PaginatorKind;
108
+ paginator?: PaginatorDescriptor;
109
+ }
110
+ /** A built-in pagination strategy. */
111
+ type PaginatorStrategy = 'page' | 'offset' | 'cursor' | 'fixed';
112
+ /** The strategy that answers a read, or `none` for an unpaginated endpoint. */
113
+ type PaginatorKind = PaginatorStrategy | 'none';
114
+ /**
115
+ * A role a page parameter fills. `page`: number, size; `offset`: offset, limit; `cursor`: after,
116
+ * before, size; `fixed`: number.
117
+ */
118
+ type PaginatorRole = 'number' | 'size' | 'offset' | 'limit' | 'after' | 'before';
119
+ /** A strategy whose roles are filled by renamed wire keys, e.g. `{ number: 'p', size: 'size' }`. */
120
+ interface StrategyDescriptor {
121
+ strategy: PaginatorStrategy;
122
+ keys: Readonly<Partial<Record<PaginatorRole, string>>>;
123
+ }
124
+ /**
125
+ * A MultiPaginator menu: the client picks an arm by its `kind` (sent as `page[kind]`), and the
126
+ * server answers with `default` when the request names none. Arm keys are the `kind` values.
127
+ */
128
+ interface MenuDescriptor {
129
+ strategy: 'multi';
130
+ default: string;
131
+ arms: Readonly<Record<string, PaginatorStrategy | StrategyDescriptor>>;
76
132
  }
77
- type PaginatorKind = 'page' | 'offset' | 'cursor' | 'none';
133
+ /**
134
+ * How an endpoint paginates. A bare kind uses the canonical wire keys (`page[number]`,
135
+ * `page[size]`, ...); a {@link StrategyDescriptor} names renamed keys; a {@link MenuDescriptor}
136
+ * offers several strategies.
137
+ */
138
+ type PaginatorDescriptor = PaginatorKind | StrategyDescriptor | MenuDescriptor;
78
139
  type ClientIdPolicy = 'forbidden' | 'optional' | 'required';
79
140
  /** Where a custom action is invoked: collection-scoped (`/{type}/-actions/{name}`) or resource-scoped (`/{type}/{id}/-actions/{name}`). */
80
141
  type ActionScope = 'collection' | 'resource';
@@ -145,7 +206,7 @@ interface ResourceDescriptor {
145
206
  relations: Readonly<Record<string, RelationDescriptor>>;
146
207
  /** Per-operation path templates, e.g. `{ fetchOne: '/albums/{id}' }`. */
147
208
  paths: Readonly<Record<string, string>>;
148
- paginator: PaginatorKind;
209
+ paginator: PaginatorDescriptor;
149
210
  clientId: ClientIdPolicy;
150
211
  /**
151
212
  * The `withCount` count tokens + negotiation profile for this type's read endpoints.
@@ -187,6 +248,168 @@ interface AtomicDescriptor {
187
248
  path: string;
188
249
  }
189
250
  //#endregion
251
+ //#region src/pagination.d.ts
252
+ /** The wire key for each role when the server keeps the default names. */
253
+ declare const CANONICAL_PAGE_KEYS: {
254
+ readonly page: {
255
+ readonly number: "number";
256
+ readonly size: "size";
257
+ };
258
+ readonly offset: {
259
+ readonly offset: "offset";
260
+ readonly limit: "limit";
261
+ };
262
+ readonly cursor: {
263
+ readonly after: "after";
264
+ readonly before: "before";
265
+ readonly size: "size";
266
+ };
267
+ readonly fixed: {
268
+ readonly number: "number";
269
+ };
270
+ readonly none: {};
271
+ };
272
+ /** Role -> wire key for the strategy that answered. */
273
+ type PageKeys = Readonly<Partial<Record<PaginatorRole, string>>>;
274
+ /** The strategy that answers one read, with its wire keys and, for a menu pick, the arm's `kind`. */
275
+ interface ResolvedPaginator {
276
+ kind: PaginatorKind;
277
+ keys: PageKeys;
278
+ /** The menu arm the request picked with `page[kind]`. Absent when it picked none (the default). */
279
+ arm?: string;
280
+ }
281
+ /**
282
+ * Resolve the strategy that answers a read from the endpoint's paginator and the `page` the
283
+ * request sends. A menu answers with the arm named by `page.kind`, else its `default`.
284
+ */
285
+ declare function resolvePaginator(paginator: PaginatorDescriptor | undefined, page?: Readonly<Record<string, unknown>>): ResolvedPaginator;
286
+ /** Navigation links by relation (`first`/`prev`/`next`/`last`), when present. */
287
+ interface PageLinks {
288
+ first?: string;
289
+ prev?: string;
290
+ next?: string;
291
+ last?: string;
292
+ }
293
+ /** Normalised pagination state on a collection, as the runtime builds it; count-free-safe. */
294
+ interface Page {
295
+ kind: PaginatorKind;
296
+ /** Role -> wire key for the strategy that answered. */
297
+ keys: PageKeys;
298
+ /** The menu arm the request picked with `page[kind]`, when it picked one. */
299
+ arm?: string;
300
+ /** The raw `meta.page` block, when present (shape varies by kind). */
301
+ meta?: Record<string, unknown>;
302
+ links: PageLinks;
303
+ }
304
+ /**
305
+ * The `meta.page` block each built-in strategy renders. `total`/`lastPage` (and `to` on an empty
306
+ * page) are absent when the server does not count.
307
+ */
308
+ type PageMeta<K extends PaginatorKind> = K extends 'page' ? {
309
+ currentPage: number;
310
+ perPage: number;
311
+ from: number;
312
+ to?: number;
313
+ total?: number;
314
+ lastPage?: number;
315
+ } : K extends 'fixed' ? {
316
+ currentPage: number;
317
+ total?: number;
318
+ lastPage?: number;
319
+ } : K extends 'offset' ? {
320
+ offset: number;
321
+ limit: number;
322
+ from: number;
323
+ to?: number;
324
+ total?: number;
325
+ } : K extends 'cursor' ? {
326
+ perPage: number;
327
+ from?: string;
328
+ to?: string;
329
+ hasMore: boolean;
330
+ } : Record<string, unknown>;
331
+ /** `$page` typed by the strategy `K` that answers, its wire keys and the picked menu arm. */
332
+ type TypedPage<K extends PaginatorKind, Keys extends PageKeys, Arm extends string | undefined = undefined> = {
333
+ kind: K;
334
+ keys: Keys;
335
+ meta?: PageMeta<K>;
336
+ links: PageLinks;
337
+ } & (Arm extends string ? {
338
+ arm: Arm;
339
+ } : {
340
+ arm?: never;
341
+ });
342
+ /** A non-menu paginator entry as `{ strategy, keys }` (a bare kind takes the canonical keys). */
343
+ type StrategyOf<P> = P extends PaginatorKind ? {
344
+ strategy: P;
345
+ keys: (typeof CANONICAL_PAGE_KEYS)[P];
346
+ } : P extends StrategyDescriptor ? P : never;
347
+ /** Cursors are opaque strings; every other role is a count. */
348
+ type RoleValue<R> = R extends 'after' | 'before' ? string : number;
349
+ /** The page members one strategy accepts, keyed by their wire names. */
350
+ type StrategyParams<S> = S extends {
351
+ keys: infer K;
352
+ } ? { -readonly [R in keyof K as K[R] extends string ? K[R] : never]?: RoleValue<R> } : never;
353
+ /** True when `P` is the loose `PaginatorDescriptor` union rather than a generated literal. */
354
+ type IsLoose<P> = [PaginatorKind] extends [P] ? true : false;
355
+ type ArmName<M extends MenuDescriptor> = Extract<keyof M['arms'], string>;
356
+ type ArmParams<M extends MenuDescriptor, A> = A extends ArmName<M> ? {
357
+ kind: A;
358
+ } & StrategyParams<StrategyOf<M['arms'][A]>> : never;
359
+ type KeysOfUnion<U> = U extends unknown ? keyof U : never;
360
+ /**
361
+ * The default arm's members without `kind`. Members only another arm reads are `never`: the
362
+ * server would select that arm by them, while the response is typed as the default.
363
+ */
364
+ type DefaultArmParams<M extends MenuDescriptor> = StrategyParams<StrategyOf<M['arms'][M['default']]>> & {
365
+ kind?: undefined;
366
+ } & { [X in Exclude<KeysOfUnion<StrategyParams<StrategyOf<M['arms'][ArmName<M>]>>>, keyof StrategyParams<StrategyOf<M['arms'][M['default']]>>>]?: never };
367
+ /**
368
+ * The `page` parameter an endpoint with paginator `P` accepts: its strategy's members under
369
+ * their wire keys (numbers, cursors as strings). A menu takes one arm tagged by `kind`, or the
370
+ * default arm's members untagged. `never` for an unpaginated endpoint; a loose record when the
371
+ * descriptor is not a generated literal.
372
+ */
373
+ type PageParams<P> = IsLoose<P> extends true ? Readonly<Record<string, unknown>> : P extends 'none' ? never : P extends MenuDescriptor ? ArmParams<P, ArmName<P>> | DefaultArmParams<P> : StrategyParams<StrategyOf<P>>;
374
+ /** The member of page parameter union `P` that argument `Q` selects by its `kind`, if any. */
375
+ type SelectedArm<Q, P> = Q extends {
376
+ kind: infer K;
377
+ } ? Extract<P, {
378
+ kind: K;
379
+ }> : Extract<P, {
380
+ kind?: undefined;
381
+ }>;
382
+ /**
383
+ * A page argument `Q` with any member outside the arm it selects turned into `never`, so a typo,
384
+ * or another arm's member, is an error.
385
+ */
386
+ type ExactPageParams<Q, P> = Q & { [K in Exclude<keyof Q, KeysOfUnion<[SelectedArm<Q, P>] extends [never] ? P : SelectedArm<Q, P>>>]: never };
387
+ type StrategyPage<S, Arm extends string | undefined = undefined> = S extends {
388
+ strategy: infer K extends PaginatorStrategy;
389
+ keys: infer Keys extends PageKeys;
390
+ } ? TypedPage<K, Keys, Arm> : Page;
391
+ type MenuPage<M extends MenuDescriptor, Q> = [Q] extends [never] ? StrategyPage<StrategyOf<M['arms'][M['default']]>> : Q extends {
392
+ kind: infer A extends ArmName<M>;
393
+ } ? StrategyPage<StrategyOf<M['arms'][A]>, A> : StrategyPage<StrategyOf<M['arms'][M['default']]>>;
394
+ /**
395
+ * The `$page` a read on an endpoint with paginator `P` carries, given the `page` argument `Q`
396
+ * it sent: the picked menu arm's strategy, the default's when it picked none, else the
397
+ * endpoint's own strategy.
398
+ */
399
+ type PageOf<P, Q = undefined> = IsLoose<P> extends true ? Page : P extends MenuDescriptor ? MenuPage<P, Q> : P extends 'none' ? TypedPage<'none', Record<never, never>> : StrategyPage<StrategyOf<P>>;
400
+ /**
401
+ * The `page` argument that reads another page from the strategy that answered `P` (a `$page`):
402
+ * its members under their wire keys, tagged with the menu arm it picked. A loose record for an
403
+ * untyped `Page`.
404
+ */
405
+ type PageParamsOf<P extends Page> = PaginatorKind extends P['kind'] ? Readonly<Record<string, string | number>> : StrategyParams<{
406
+ keys: P['keys'];
407
+ }> & (P extends {
408
+ arm: infer A extends string;
409
+ } ? {
410
+ kind: A;
411
+ } : unknown);
412
+ //#endregion
190
413
  //#region src/materialise.d.ts
191
414
  /** A trimmed, by-reference top-level envelope shared by every resource from one response. */
192
415
  interface DocumentEnvelope {
@@ -204,19 +427,6 @@ interface Edge {
204
427
  links?: Record<string, unknown>;
205
428
  meta?: Record<string, unknown>;
206
429
  }
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
430
  /** The uniform shape `$rel(name)` returns. */
221
431
  interface RelationView {
222
432
  data?: unknown;
@@ -257,10 +467,11 @@ interface MaterialiseContext {
257
467
  * Re-fetch + re-materialise a link (wired in Build 4); returns the materialised value.
258
468
  * `linkage` propagates the originating surface so a paginated relationship-endpoint
259
469
  * array re-materialises as identifiers (not resources) on `$next()`/`$prev()`. `paginator`
260
- * carries the current page's kind so the re-materialised page reports the same `$page.kind`
261
- * (a divergent per-relation paginator, or an EMPTY next page that carries no member to sniff).
470
+ * carries the strategy that answered the current page, so the re-materialised page reports the
471
+ * same `$page` (a divergent per-relation paginator, a picked menu arm, or an EMPTY next page
472
+ * that carries no member to sniff).
262
473
  */
263
- navigate: (url: string, linkage?: boolean, paginator?: PaginatorKind, signal?: AbortSignal) => Promise<unknown>;
474
+ navigate: (url: string, linkage?: boolean, paginator?: ResolvedPaginator, signal?: AbortSignal) => Promise<unknown>;
264
475
  /**
265
476
  * The opt-in per-resource validator (ADR 0004), resolved from `ClientOptions.validate`. When
266
477
  * present, every wire resource in `data`/`included` is validated against its per-type schema
@@ -301,14 +512,15 @@ interface RawRelationship {
301
512
  * full resource) — a structural guess can't tell them apart for an attribute-less,
302
513
  * relation-less resource, so the caller passes the surface it issued.
303
514
  *
304
- * `paginatorOverride` sets the top-level array's `$page.kind` for a related/relationship read,
305
- * where the paginator is the RELATION's (which may diverge from the related type's collection —
306
- * D6a) and an empty page carries no member to sniff a type from. `primaryType` is preferred for a
515
+ * `paginatorOverride` sets the top-level array's `$page` for a related/relationship read, where the
516
+ * paginator is the RELATION's (which may diverge from the related type's collection — D6a), for a
517
+ * menu read that picked an arm, and where an empty page carries no member to sniff a type from. A
518
+ * bare kind is read with its canonical wire keys. `primaryType` is preferred for a
307
519
  * top-level collection read (statically-known); both override the sniff-from-`data[0]` fallback.
308
520
  */
309
- declare function materialise(doc: Document, ctx: MaterialiseContext, linkage?: boolean, primaryType?: string, paginatorOverride?: PaginatorKind): unknown;
521
+ declare function materialise(doc: Document, ctx: MaterialiseContext, linkage?: boolean, primaryType?: string, paginatorOverride?: ResolvedPaginator | PaginatorKind): unknown;
310
522
  /**
311
- * A to-many relation's paginator kind. The relation's OWN related/relationship endpoint may
523
+ * A to-many relation's paginator. The relation's OWN related/relationship endpoint may
312
524
  * paginate differently than the related type's top-level collection (D6a), so prefer the
313
525
  * per-relation `paginator` (carried in the descriptor only when it diverges); otherwise fall back
314
526
  * to the related type's own paginator (the relationship and related endpoints both paginate that
@@ -316,6 +528,8 @@ declare function materialise(doc: Document, ctx: MaterialiseContext, linkage?: b
316
528
  * resolves off its first declared related type (its members span types, matching the relationship
317
529
  * endpoint's first-member resolution).
318
530
  */
531
+ declare function relatedPaginator(relation: RelationDescriptor | undefined, ctx: MaterialiseContext): PaginatorDescriptor | undefined;
532
+ /** The kind a relation's related read answers with when it sends no `page`. See {@link relatedPaginator}. */
319
533
  declare function relatedPaginatorKind(relation: RelationDescriptor | undefined, ctx: MaterialiseContext): PaginatorKind;
320
534
  //#endregion
321
535
  //#region src/types.d.ts
@@ -489,11 +703,13 @@ declare function runAtomic<D extends ApiDescriptor, A, W, const Ops>(request: Js
489
703
  * a real, read-only `T[]` carrying the non-enumerable relationship-level envelope
490
704
  * (`$page`/`$links`/`$meta`) plus link-driven `$next()`/`$prev()` navigation. The same
491
705
  * model backs top-level collections, related-endpoint collections, and hydrated/linkage
492
- * to-many relationship values ("one model, three surfaces").
706
+ * to-many relationship values ("one model, three surfaces"). `P` types `$page` by the strategy
707
+ * that answered (see {@link PageOf}); following a link keeps it.
493
708
  */
494
- type Collection<T> = ReadonlyArray<T> & Omit<ArrayEnvelope, '$next' | '$prev'> & {
495
- $next(options?: NavigateOptions): Promise<Collection<T> | undefined>;
496
- $prev(options?: NavigateOptions): Promise<Collection<T> | undefined>;
709
+ type Collection<T, P = Page> = ReadonlyArray<T> & Omit<ArrayEnvelope, '$page' | '$next' | '$prev'> & {
710
+ readonly $page: P;
711
+ $next(options?: NavigateOptions): Promise<Collection<T, P> | undefined>;
712
+ $prev(options?: NavigateOptions): Promise<Collection<T, P> | undefined>;
497
713
  };
498
714
  /**
499
715
  * The edge-local accessors carried by a materialised related VALUE (a per-edge view):
@@ -518,6 +734,12 @@ interface ResourceAccessors {
518
734
  type TypeName<D extends ApiDescriptor> = Extract<keyof D, string>;
519
735
  /** The relation names declared on type `T` of descriptor `D`. */
520
736
  type RelationName<D extends ApiDescriptor, T extends TypeName<D>> = Extract<keyof D[T]['relations'], string>;
737
+ /** The relations of type `T` a response never renders (the descriptor's `writeOnly: true`). */
738
+ type WriteOnlyRelationName<D extends ApiDescriptor, T extends TypeName<D>> = { [R in RelationName<D, T>]: D[T]['relations'][R] extends {
739
+ writeOnly: true;
740
+ } ? R : never }[RelationName<D, T>];
741
+ /** The relations of type `T` a response can render: every relation but the write-only ones. */
742
+ type ReadableRelationName<D extends ApiDescriptor, T extends TypeName<D>> = Exclude<RelationName<D, T>, WriteOnlyRelationName<D, T>>;
521
743
  /** The attribute object for type `T`, from the generated attributes map `A` (falls back to `{}`). */
522
744
  type AttributesOf<A, T> = T extends keyof A ? A[T] : Record<string, never>;
523
745
  /**
@@ -553,6 +775,15 @@ type PickSelected<Obj, Sel> = [Sel] extends [AllFields] ? Obj : Pick<Obj, Extrac
553
775
  type RelatedType<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = Extract<D[T]['relations'][R]['types'][number], TypeName<D>>;
554
776
  /** True when relation `R` of type `T` is to-many. */
555
777
  type IsMany<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = D[T]['relations'][R]['cardinality'] extends 'many' ? true : false;
778
+ /**
779
+ * The paginator relation `R`'s related/relationship endpoints use: the relation's own when it
780
+ * diverges (D6a), else its first related type's collection paginator (as the runtime resolves).
781
+ */
782
+ type RelationPaginator<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = D[T]['relations'][R] extends {
783
+ paginator: infer P;
784
+ } ? P : D[T]['relations'][R]['types'][0] extends infer First extends TypeName<D> ? D[First]['paginator'] : 'none';
785
+ /** The `$page` of relation `R`'s to-many value read with page argument `Q` (none = the default). */
786
+ type RelationPage<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>, Q = undefined> = PageOf<RelationPaginator<D, T, R>, Q>;
556
787
  /**
557
788
  * The bare resource-identifier shape a non-included (but linked) relation takes,
558
789
  * carrying the per-edge `$edge`/`$pivot` accessors the runtime attaches to every
@@ -584,13 +815,13 @@ type Identifier<TType extends string, Pivot = Record<string, unknown>> = TType e
584
815
  * (Excluded to-many stays a `Collection`: linkage may still be present, and a lazy
585
816
  * to-many materialises as an augmented array regardless — only its members differ.)
586
817
  */
587
- 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;
818
+ 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>, RelationPage<D, T, R>> : Hydrated<D, A, RelatedType<D, T, R>, F> | null : IsMany<D, T, R> extends true ? Collection<Identifier<RelatedType<D, T, R>>, RelationPage<D, T, R>> : Identifier<RelatedType<D, T, R>> | null | undefined;
588
819
  /**
589
820
  * The relation slots of type `T`, each typed by cardinality and include-presence, then
590
821
  * narrowed to the fieldset selection for `T` (relations are sparse-fieldset members too — an
591
- * unrequested relation is statically absent).
822
+ * unrequested relation is statically absent). A write-only relation has no slot.
592
823
  */
593
- 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>>;
824
+ type Relations<D extends ApiDescriptor, A, T extends TypeName<D>, Inc, F = unknown> = PickSelected<{ [R in ReadableRelationName<D, T>]: RelationValue<D, A, T, R, Inc, F> }, SelectedFields<F, T>>;
594
825
  /**
595
826
  * A materialised resource object: flattened `type` + `id` + attributes + relation slots
596
827
  * as own enumerable props, intersected with the resource-level `$`-accessors. `Inc` is
@@ -665,18 +896,20 @@ type FilterQuery<D extends ApiDescriptor, T extends TypeName<D>> = [FilterKey<D,
665
896
  type FieldsMap<D extends ApiDescriptor> = { [T in TypeName<D>]?: readonly MemberName<D, T>[] };
666
897
  /**
667
898
  * A selectable sparse-fieldset member of type `T`: an attribute name (the descriptor's
668
- * `attributes` keys) or a relation name. The descriptor carries the literal member names, so
669
- * `fields` entries are constrained to real members.
899
+ * `attributes` keys) or a relation a response renders. The descriptor carries the literal member
900
+ * names, so `fields` entries are constrained to real members.
670
901
  */
671
- type MemberName<D extends ApiDescriptor, T extends TypeName<D>> = Extract<keyof D[T]['attributes'], string> | RelationName<D, T>;
902
+ type MemberName<D extends ApiDescriptor, T extends TypeName<D>> = Extract<keyof D[T]['attributes'], string> | ReadableRelationName<D, T>;
903
+ /** The `page` argument a collection read of type `T` accepts (see {@link PageParams}). */
904
+ type ListPageParams<D extends ApiDescriptor, T extends TypeName<D>> = PageParams<D[T]['paginator']>;
672
905
  /**
673
906
  * A read query carrying a typed, narrowing `include`, a typed `fields` (drives sparse-fieldset
674
- * return narrowing), a `withCount` constrained to the type's count tokens, plus the loose
675
- * JSON:API query families. `Inc`/`F` are inferred (as `const`) at the call site. Used for the
907
+ * return narrowing), a `withCount` constrained to the type's count tokens, a `page` typed by the
908
+ * collection's paginator (`Pg` drives the `$page` type), plus the loose JSON:API query families. `Inc`/`F` are inferred (as `const`) at the call site. Used for the
676
909
  * COLLECTION read (`list`); the single-resource read (`get`) uses {@link SingleReadQuery}, which
677
910
  * drops `withCount` (no `GET /{type}/{id}` endpoint advertises it).
678
911
  */
679
- interface TypedReadQuery<D extends ApiDescriptor, T extends TypeName<D>, Inc extends readonly IncludePath<D, T>[], F extends FieldsMap<D> = FieldsMap<D>> {
912
+ interface TypedReadQuery<D extends ApiDescriptor, T extends TypeName<D>, Inc extends readonly IncludePath<D, T>[], F extends FieldsMap<D> = FieldsMap<D>, Pg extends ListPageParams<D, T> | undefined = ListPageParams<D, T> | undefined> {
680
913
  /** The relations to include — drives static narrowing; constrained to the advertised paths. */
681
914
  include?: Inc;
682
915
  /** Filter the collection — keys constrained to the type's advertised `filter[...]` params. */
@@ -687,7 +920,11 @@ interface TypedReadQuery<D extends ApiDescriptor, T extends TypeName<D>, Inc ext
687
920
  fields?: F;
688
921
  /** Relationship-count tokens (the Countable profile) — comma-joined onto `withCount`. */
689
922
  withCount?: readonly CountToken<D, T>[];
690
- page?: Record<string, unknown>;
923
+ /**
924
+ * The page to read, under the paginator's wire keys. On a menu, `kind` picks a strategy;
925
+ * without it the server answers with its default.
926
+ */
927
+ page?: ExactPageParams<Pg, ListPageParams<D, T>>;
691
928
  /** Cancels the read when signalled (e.g. a superseded request). Not sent to the server. */
692
929
  signal?: AbortSignal;
693
930
  }
@@ -711,13 +948,16 @@ interface SingleReadQuery<D extends ApiDescriptor, T extends TypeName<D>, Inc ex
711
948
  * narrowed to the requested sparse fieldset.
712
949
  */
713
950
  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>;
714
- /** A loose read query (filter/sort/include/fields/page/withCount) for the relationship/related endpoints. */
715
- interface RelationReadQuery {
951
+ /**
952
+ * A read query for the relationship/related endpoints: loose filter/sort/include/fields/withCount,
953
+ * and a `page` of type `Pg` (typed from the relation's paginator on a typed accessor).
954
+ */
955
+ interface RelationReadQuery<Pg = Readonly<Record<string, unknown>>> {
716
956
  filter?: Record<string, unknown>;
717
957
  sort?: string | readonly string[];
718
958
  include?: readonly string[];
719
959
  fields?: Record<string, readonly string[]>;
720
- page?: Record<string, unknown>;
960
+ page?: Pg;
721
961
  /**
722
962
  * Relationship-count tokens (`_self_` + the relation's countable relations) — the related/
723
963
  * relationship endpoints advertise their own `withCount`, and the client negotiates the
@@ -729,27 +969,64 @@ interface RelationReadQuery {
729
969
  }
730
970
  /** The TS type a descriptor wire-format hint maps to (mirrors the runtime coercion vocabulary). */
731
971
  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;
732
- /**
733
- * The typed `meta.pivot` shape of relation `R` — built from the descriptor's per-relation
734
- * `pivotFields` (field-name → format hint), e.g. `{ position: number; weight: number; addedAt:
735
- * string }`. Falls back to a loose record when the relation carries no pivot fields (D33).
972
+ /** The string members of a descriptor list, or `never` when it is absent. */
973
+ type ListMember<L> = L extends readonly (infer M)[] ? M & string : never;
974
+ /** The fields of a {@link PivotDescriptor} every member carries (all of them when `required` is absent). */
975
+ type RequiredPivotField<P> = P extends {
976
+ fields: infer Fl;
977
+ } ? P extends {
978
+ required: infer Req;
979
+ } ? Extract<keyof Fl, ListMember<Req>> : keyof Fl : never;
980
+ /** One pivot field's TS type: its format hint's type, plus `null` when the field is nullable. */
981
+ type PivotValue<P, K> = P extends {
982
+ fields: infer Fl;
983
+ } ? K extends keyof Fl ? FormatToTs<Fl[K]> | (K extends ListMember<P extends {
984
+ nullable: infer N;
985
+ } ? N : never> ? null : never) : never : never;
986
+ /** The fields of a {@link PivotDescriptor}. */
987
+ type PivotFieldNames<P> = P extends {
988
+ fields: infer Fl;
989
+ } ? keyof Fl : never;
990
+ /** The `meta.pivot` object a {@link PivotDescriptor} describes. */
991
+ type PivotObject<P> = { readonly [K in PivotFieldNames<P> as K extends RequiredPivotField<P> ? K : never]: PivotValue<P, K> } & { readonly [K in PivotFieldNames<P> as K extends RequiredPivotField<P> ? never : K]?: PivotValue<P, K> };
992
+ /** The legacy hint-only pivot shape: every field present and non-null. */
993
+ type LegacyPivot<P> = { readonly [K in keyof P]: FormatToTs<P[K]> };
994
+ /**
995
+ * The typed `meta.pivot` a response renders on each member of relation `R`, from the descriptor's
996
+ * `readPivot`, e.g. `{ position: number; weight: number | null; addedAt: string }`. A descriptor
997
+ * with only `pivotFields` types every field present and non-null; one with neither falls back to
998
+ * a loose record (D33).
736
999
  */
737
1000
  type PivotFieldsOf<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = D[T]['relations'][R] extends {
1001
+ readPivot: infer P;
1002
+ } ? PivotObject<P> : D[T]['relations'][R] extends {
738
1003
  pivotFields: infer P;
739
- } ? { readonly [K in keyof P]: FormatToTs<P[K]> } : Record<string, unknown>;
1004
+ } ? LegacyPivot<P> : Record<string, unknown>;
1005
+ /**
1006
+ * The typed `meta.pivot` a request may send on each member of relation `R`, from the descriptor's
1007
+ * `writePivot` (only writable fields, optional unless required). Falls back like
1008
+ * {@link PivotFieldsOf} when the descriptor has no `writePivot`.
1009
+ */
1010
+ type WritePivotOf<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = D[T]['relations'][R] extends {
1011
+ writePivot: infer P;
1012
+ } ? PivotObject<P> : D[T]['relations'][R] extends {
1013
+ pivotFields: infer P;
1014
+ } ? LegacyPivot<P> : PivotInput;
740
1015
  /**
741
1016
  * A hydrated related VALUE for relation `R` of type `T`, resolved by cardinality over the
742
1017
  * (possibly polymorphic) related type(s): a `Collection` of hydrated members for to-many,
743
1018
  * the hydrated member (or `null`) for to-one. Each member's `$pivot` is typed from the
744
- * relation's `pivotFields` (D33).
1019
+ * relation's `readPivot` (D33).
745
1020
  */
746
- 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;
1021
+ type RelatedValue<D extends ApiDescriptor, A, T extends TypeName<D>, R extends RelationName<D, T>, Q = undefined> = IsMany<D, T, R> extends true ? Collection<Hydrated<D, A, RelatedType<D, T, R>, unknown, PivotFieldsOf<D, T, R>>, RelationPage<D, T, R, Q>> : Hydrated<D, A, RelatedType<D, T, R>, unknown, PivotFieldsOf<D, T, R>> | null;
747
1022
  /**
748
1023
  * A linkage VALUE for relation `R` (the `/relationships/{rel}` endpoint, identifiers only):
749
1024
  * a `Collection` of identifier members for to-many, a single identifier (or `null`) for to-one.
750
- * Each member's `$pivot` is typed from the relation's `pivotFields` (D33).
1025
+ * Each member's `$pivot` is typed from the relation's `readPivot` (D33).
751
1026
  */
752
- 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;
1027
+ type LinkageValue<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>, Q = undefined> = IsMany<D, T, R> extends true ? Collection<Identifier<RelatedType<D, T, R>, PivotFieldsOf<D, T, R>>, RelationPage<D, T, R, Q>> : Identifier<RelatedType<D, T, R>, PivotFieldsOf<D, T, R>> | null;
1028
+ /** The `page` argument relation `R`'s to-many reads accept; `never` for a to-one. */
1029
+ type RelationPageParams<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = IsMany<D, T, R> extends true ? PageParams<RelationPaginator<D, T, R>> : never;
753
1030
  /**
754
1031
  * A relationship accessor off a {@link ResourceHandle} (`client.albums.id('1').tracks`):
755
1032
  * reads — `.get()` (linkage) / `.related()` (related collection); writes — to-many
@@ -763,27 +1040,30 @@ type LinkageValue<D extends ApiDescriptor, T extends TypeName<D>, R extends Rela
763
1040
  * linkage, or `void` when the server replies `204`.
764
1041
  */
765
1042
  interface RelationshipAccessor<D extends ApiDescriptor, A, T extends TypeName<D>, R extends RelationName<D, T>> {
766
- get: ExposesRelationship<D, T, R> extends true ? (query?: RelationReadQuery) => Promise<LinkageValue<D, T, R>> : never;
767
- related: ExposesRelated<D, T, R> extends true ? (query?: RelationReadQuery) => Promise<RelatedValue<D, A, T, R>> : never;
1043
+ get: ReadsRelationship<D, T, R> extends true ? <const Pg extends RelationPageParams<D, T, R> | undefined = undefined>(query?: RelationReadQuery<ExactPageParams<Pg, RelationPageParams<D, T, R>>>) => Promise<LinkageValue<D, T, R, Pg>> : never;
1044
+ related: ExposesRelated<D, T, R> extends true ? <const Pg extends RelationPageParams<D, T, R> | undefined = undefined>(query?: RelationReadQuery<ExactPageParams<Pg, RelationPageParams<D, T, R>>>) => Promise<RelatedValue<D, A, T, R, Pg>> : never;
768
1045
  add: RelationMutation<D, T, R, 'add', readonly WriteRef<D, T, R>[]>;
769
1046
  remove: RelationMutation<D, T, R, 'remove', readonly LinkageRefOf<RelatedType<D, T, R>>[]>;
770
1047
  replace: RelationMutation<D, T, R, 'replace', readonly WriteRef<D, T, R>[]>;
771
1048
  set: RelationMutation<D, T, R, 'set', LinkageRefOf<RelatedType<D, T, R>> | null>;
772
1049
  }
773
1050
  /**
774
- * A linkage ref for a to-many membership WRITE (`add`/`replace`) on relation `R`, with `$pivot`
775
- * typed from the relation's `pivotFields` — so a `belongsToMany` add carries the precise pivot
776
- * shape (`{ position, weight, … }`) instead of a loose record (D33).
1051
+ * A linkage ref for a to-many membership WRITE on relation `R` (`add`/`replace`, or a create or
1052
+ * update input), with `$pivot` typed from the relation's `writePivot` — so a `belongsToMany` add
1053
+ * carries the precise pivot shape (`{ position, weight?, … }`) instead of a loose record (D33).
777
1054
  */
778
1055
  type WriteRef<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = LinkageRefOf<RelatedType<D, T, R>> & {
779
- $pivot?: PivotFieldsOf<D, T, R>;
1056
+ $pivot?: WritePivotOf<D, T, R>;
780
1057
  };
781
1058
  /**
782
1059
  * True unless relation `R`'s related endpoint is suppressed (`related: false`, the bundle's
783
- * `withoutRelatedEndpoint()`). Absent flag ⇒ exposed (the default / a hand-written descriptor).
1060
+ * `withoutRelatedEndpoint()`) or the relation is write-only. Absent flag ⇒ exposed (the default /
1061
+ * a hand-written descriptor).
784
1062
  */
785
1063
  type ExposesRelated<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = D[T]['relations'][R] extends {
786
1064
  related: false;
1065
+ } | {
1066
+ writeOnly: true;
787
1067
  } ? false : true;
788
1068
  /**
789
1069
  * True unless relation `R`'s relationship endpoint is suppressed (`relationship: false`, the
@@ -792,6 +1072,10 @@ type ExposesRelated<D extends ApiDescriptor, T extends TypeName<D>, R extends Re
792
1072
  type ExposesRelationship<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = D[T]['relations'][R] extends {
793
1073
  relationship: false;
794
1074
  } ? false : true;
1075
+ /** Whether relation `R`'s linkage can be read back: exposed, and not write-only. */
1076
+ type ReadsRelationship<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = D[T]['relations'][R] extends {
1077
+ writeOnly: true;
1078
+ } ? false : ExposesRelationship<D, T, R>;
795
1079
  /** The mutation verb a relationship accessor method maps to (`add`/`remove`/`replace` to-many, `set` to-one). */
796
1080
  type MutationVerb = 'add' | 'remove' | 'replace' | 'set';
797
1081
  /** The cardinality a mutation verb belongs to (`set` is to-one; the rest are to-many). */
@@ -818,9 +1102,9 @@ type RelationMutation<D extends ApiDescriptor, T extends TypeName<D>, R extends
818
1102
  /** The relationship accessors of type `T`, keyed by relation name. */
819
1103
  type RelationshipAccessors<D extends ApiDescriptor, A, T extends TypeName<D>> = { [R in RelationName<D, T>]: RelationshipAccessor<D, A, T, R> };
820
1104
  /**
821
- * Writable pivot data carried on a to-many member as `$pivot` (a belongsToMany relation
822
- * with `pivot: true`). Loose by design — the OpenAPI write attributes don't model per-edge
823
- * pivot fields, so the runtime passes the object through as `meta.pivot`.
1105
+ * Writable pivot data carried on a to-many member as `$pivot` (a belongsToMany relation with
1106
+ * `pivot: true`), passed through as `meta.pivot`. The loose fallback for a relation whose
1107
+ * descriptor has no `writePivot`.
824
1108
  */
825
1109
  type PivotInput = Record<string, unknown>;
826
1110
  /**
@@ -841,10 +1125,14 @@ type LinkageRef<TType extends string> = (ResourceIdentifier<TType> & {
841
1125
  type LinkageRefOf<TType extends string> = TType extends string ? LinkageRef<TType> : never;
842
1126
  /**
843
1127
  * The relationship VALUE accepted in a create/update input slot, by cardinality:
844
- * to-one accepts a single ref or `null` (clear); to-many accepts an array of refs.
1128
+ * to-one accepts a single ref or `null` (clear); to-many accepts an array of refs, each with an
1129
+ * optional `$pivot` typed from the relation's `writePivot`.
1130
+ */
1131
+ type RelationInput<D extends ApiDescriptor, T extends TypeName<D>, R extends RelationName<D, T>> = IsMany<D, T, R> extends true ? readonly WriteRef<D, T, R>[] : LinkageRefOf<RelatedType<D, T, R>> | null;
1132
+ /**
1133
+ * The relationship slots of type `T` as optional write input keys (one per declared relation,
1134
+ * write-only ones included).
845
1135
  */
846
- 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;
847
- /** The relationship slots of type `T` as optional write input keys (one per declared relation). */
848
1136
  type RelationInputs<D extends ApiDescriptor, T extends TypeName<D>> = { [R in RelationName<D, T>]?: RelationInput<D, T, R> };
849
1137
  /** The `{ create; update }` write-attribute pair for type `T` (or an open pair when `T` is absent from `W`). */
850
1138
  type WritePair<W, T> = T extends keyof W ? W[T] : {
@@ -993,7 +1281,7 @@ type ResourceHandle<D extends ApiDescriptor, A, W, T extends TypeName<D>, Act =
993
1281
  } & RelationshipAccessors<D, A, T>;
994
1282
  /** The collection-scoped accessor for one wire type (`client.albums`). */
995
1283
  interface TypeAccessor<D extends ApiDescriptor, A, W, T extends TypeName<D>, Act = DefaultActionTypes> {
996
- 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>>>;
1284
+ list<const Inc extends readonly IncludePath<D, T>[] = [], const F extends FieldsMap<D> = {}, const Pg extends ListPageParams<D, T> | undefined = undefined>(query?: TypedReadQuery<D, T, Inc, F, Pg>): Promise<Collection<ReadResult<D, A, T, Inc, F>, PageOf<D[T]['paginator'], Pg>>>;
997
1285
  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>>;
998
1286
  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>>;
999
1287
  id(id: string): ResourceHandle<D, A, W, T, Act>;
@@ -1004,9 +1292,10 @@ interface TypeAccessor<D extends ApiDescriptor, A, W, T extends TypeName<D>, Act
1004
1292
  * The descriptor-driven client surface: one {@link TypeAccessor} per wire type. `W` is the
1005
1293
  * generated `WriteAttributes` map (per-type `{ create; update }` attribute pairs) and `Act`
1006
1294
  * the generated per-action body-type map; both thread through to the write/action surfaces and
1007
- * default so read-only / codegen-less callers compile.
1295
+ * default so read-only / codegen-less callers compile. `E` is the server's documented
1296
+ * error-code union, carried at the type level only (read it back with `ErrorCodeOf`).
1008
1297
  */
1009
- type Client<D extends ApiDescriptor, A = DefaultAttributes<D>, W = DefaultWriteAttributes<D>, Act = DefaultActionTypes> = { [T in TypeName<D>]: TypeAccessor<D, A, W, T, Act> } & {
1298
+ type Client<D extends ApiDescriptor, A = DefaultAttributes<D>, W = DefaultWriteAttributes<D>, Act = DefaultActionTypes, E extends string = string> = { [T in TypeName<D>]: TypeAccessor<D, A, W, T, Act> } & {
1010
1299
  /**
1011
1300
  * Run an Atomic Operations batch (CONTEXT.md "Atomic — typed transaction builder"): the
1012
1301
  * callback records `create`/`update`/`delete` ops on a non-type-scoped recorder (each carries
@@ -1024,7 +1313,13 @@ type Client<D extends ApiDescriptor, A = DefaultAttributes<D>, W = DefaultWriteA
1024
1313
  * - return void/nothing -> the loose, ordered `AtomicResult[]` (backward-compatible).
1025
1314
  */
1026
1315
  atomic<const Ops>(build: (tx: AtomicRecorder<D, W>) => Ops): Promise<Ops extends readonly AtomicHandle[] ? AtomicResults<D, A, Ops> : AtomicResult[]>;
1316
+ readonly [errorCodes]?: E;
1027
1317
  };
1318
+ declare const errorCodes: unique symbol;
1319
+ /** The error-code union a client type carries, or `string` for one that carries none. */
1320
+ type ErrorCodeOf<T> = T extends {
1321
+ readonly [errorCodes]?: infer C extends string;
1322
+ } ? C : string;
1028
1323
  /**
1029
1324
  * The fallback attribute map when a caller doesn't supply the generated `Attributes`
1030
1325
  * (direct `createClient` users without codegen): every type's attributes are an open
@@ -1113,9 +1408,9 @@ declare const HANDLE_RESERVED: ReadonlySet<string>;
1113
1408
  * not per-type codegen: `client.<type>` yields a {@link TypeAccessor}, `.id(id)` a handle,
1114
1409
  * and a handle's relation accessors drive the relationship/related endpoints. Navigation
1115
1410
  * (`$next()`/`$prev()`, related links) is wired through `materialise`'s `navigate` seam,
1116
- * which executes the (absolute) link and re-materialises.
1411
+ * which resolves the server's link against `baseUrl`, executes it and re-materialises.
1117
1412
  */
1118
- declare function createClient<D extends ApiDescriptor, A = DefaultAttributes<D>, W = DefaultWriteAttributes<D>, Act = DefaultActionTypes>(descriptor: D, options: ClientOptions): Client<D, A, W, Act>;
1413
+ declare function createClient<D extends ApiDescriptor, A = DefaultAttributes<D>, W = DefaultWriteAttributes<D>, Act = DefaultActionTypes, E extends string = string>(descriptor: D, options: ClientOptions): Client<D, A, W, Act, E>;
1119
1414
  //#endregion
1120
1415
  //#region src/request.d.ts
1121
1416
  /**
@@ -1150,8 +1445,17 @@ interface JsonApiContext {
1150
1445
  /** A single read/write request, before serialisation. */
1151
1446
  interface JsonApiRequest {
1152
1447
  method: string;
1153
- /** A path relative to `baseUrl`, or an absolute `http(s)://` URL (e.g. a page link). */
1448
+ /**
1449
+ * A path appended to `baseUrl` (a filled path template such as `/albums/1`), or an absolute
1450
+ * `http(s)://` URL. With {@link link} set, a link from the server instead.
1451
+ */
1154
1452
  path: string;
1453
+ /**
1454
+ * `path` is a link the server sent (a `links` member such as `next`, or a `Location` header).
1455
+ * It resolves against `baseUrl` per RFC 3986, so a root-relative `/api/v5/albums?page[number]=2`
1456
+ * keeps the base's origin but not its path, where a path template is appended to the base.
1457
+ */
1458
+ link?: boolean;
1155
1459
  query?: ReadQuery;
1156
1460
  body?: unknown;
1157
1461
  /**
@@ -1195,8 +1499,8 @@ interface JsonApiRequest {
1195
1499
  */
1196
1500
  declare function serializeQuery(query: ReadQuery): string;
1197
1501
  /**
1198
- * Execute a request: build the URL (verbatim when already absolute — for page links —
1199
- * else `baseUrl + path`), apply content negotiation and any async per-request headers,
1502
+ * Execute a request: build the URL (a server link resolved against `baseUrl`, an absolute path
1503
+ * verbatim, else `baseUrl + path`), apply content negotiation and any async per-request headers,
1200
1504
  * then drive the transport. A 2xx with a body parses to a {@link Document}; an empty /
1201
1505
  * 204 body resolves to `undefined`. A non-2xx throws a {@link JsonApiError} carrying the
1202
1506
  * parsed error objects (tolerating a non-JSON error body with an empty list).
@@ -1270,5 +1574,5 @@ declare function assertJsonApiDocument(body: unknown): asserts body is Document;
1270
1574
  */
1271
1575
  declare function validateDocument(body: Document, validator: ResolvedValidator): void;
1272
1576
  //#endregion
1273
- export { SortQuery as $, DefaultActionTypes as A, Page as At, IncludePath as B, ApiDescriptor as Bt, fetchTransport as C, mediaType as Ct, Collection as D, EdgeEnvelope as Dt, Client as E, Edge as Et, FieldsMap as F, relatedPaginatorKind as Ft, ReadResult as G, PaginatorKind as Gt, LinkageRef as H, Cardinality as Ht, FilterKey as I, ActionDescriptor as It, RelationshipAccessor as J, ResourceDescriptor as Jt, RelationName as K, RelationDescriptor as Kt, FilterQuery as L, ActionInput as Lt, DefaultWriteAttributes as M, ResourceEnvelope as Mt, EdgeMembers as N, ResourceLinks as Nt, CountToken as O, MaterialiseContext as Ot, FieldSelectionMap as P, materialise as Pt, SingleReadQuery as Q, HydratedMember as R, ActionOutput as Rt, TransportResponse as S, ResourceIdentifier as St, AtomicResultOf as T, DocumentEnvelope as Tt, MemberName as U, ClientIdPolicy as Ut, IncludedRelations as V, AtomicDescriptor as Vt, PivotInput as W, CountableDescriptor 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, RelationView as jt, CreateInput as k, NavigateOptions 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, RelationMutations 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, ActionScope as zt };
1274
- //# sourceMappingURL=validate-CfrCMZkA.d.mts.map
1577
+ export { RelationshipAccessor as $, ActionDescriptor as $t, DefaultActionTypes as A, DocumentEnvelope as At, IdentifierMember as B, relatedPaginatorKind as Bt, fetchTransport as C, ATOMIC_MEDIA_TYPE as Ct, Collection as D, ResourceIdentifier as Dt, Client as E, MediaTypeParams as Et, FieldSelectionMap as F, RelationView as Ft, MemberName as G, PageLinks as Gt, IncludedRelations as H, ExactPageParams as Ht, FieldsMap as I, ResourceEnvelope as It, ReadableRelationName as J, PageParams as Jt, PivotInput as K, PageMeta as Kt, FilterKey as L, ResourceLinks as Lt, DefaultWriteAttributes as M, EdgeEnvelope as Mt, EdgeMembers as N, MaterialiseContext as Nt, CountToken as O, mediaType as Ot, ErrorCodeOf as P, NavigateOptions as Pt, RelationReadQuery as Q, resolvePaginator as Qt, FilterQuery as R, materialise as Rt, TransportResponse as S, ATOMIC_EXT as St, AtomicResultOf as T, LocalIdentifier as Tt, LinkageRef as U, Page as Ut, IncludePath as V, CANONICAL_PAGE_KEYS as Vt, ListPageParams as W, PageKeys as Wt, RelationPageParams as X, ResolvedPaginator as Xt, RelationName as Y, PageParamsOf as Yt, RelationPaginator as Z, TypedPage as Zt, HANDLE_RESERVED as _, StrategyDescriptor as _n, AtomicResult as _t, Validator as a, Cardinality as an, SortToken as at, JsonApiTransport as b, AtomicUpdateInput as bt, resolveValidator as c, MenuDescriptor as cn, TypedReadQuery as ct, JsonApiContext as d, PaginatorRole as dn, AtomicCreateHandle as dt, ActionInput as en, ResourceAccessors as et, JsonApiRequest as f, PaginatorStrategy as fn, AtomicCreateInput as ft, ClientOptions as g, ResourceDescriptor as gn, AtomicRef as gt, serializeQuery as h, RelationMutations as hn, AtomicRecorder as ht, ValidationOption as i, AtomicDescriptor as in, SortQuery as it, DefaultAttributes as j, Edge as jt, CreateInput as k, ArrayEnvelope as kt, validateDocument as l, PaginatorDescriptor as ln, UpdateInput as lt, execute as m, RelationDescriptor as mn, AtomicHandle as mt, SchemaMap as n, ActionScope as nn, ResourceObjectView as nt, WireResource as o, ClientIdPolicy as on, TypeAccessor as ot, ReadQuery as p, PivotDescriptor as pn, AtomicDeleteHandle as pt, ReadResult as q, PageOf as qt, ValidationConfig as r, ApiDescriptor as rn, SingleReadQuery as rt, assertJsonApiDocument as s, CountableDescriptor as sn, TypeName as st, ResolvedValidator as t, ActionOutput as tn, ResourceHandle as tt, Document as u, PaginatorKind as un, WriteOptions as ut, HeadersInit as v, AtomicResults as vt, ActionsAccessor as w, JSON_API_MEDIA_TYPE as wt, TransportRequest as x, runAtomic as xt, createClient as y, AtomicUpdateHandle as yt, HydratedMember as z, relatedPaginator as zt };
1578
+ //# sourceMappingURL=validate-CJPivkqT.d.mts.map