@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.
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/dist/ajv.cjs +62 -0
- package/dist/ajv.d.cts +55 -0
- package/dist/ajv.d.cts.map +1 -0
- package/dist/ajv.d.mts +55 -0
- package/dist/ajv.d.mts.map +1 -0
- package/dist/ajv.mjs +62 -0
- package/dist/ajv.mjs.map +1 -0
- package/dist/index.cjs +1254 -0
- package/dist/index.d.cts +198 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +198 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1232 -0
- package/dist/index.mjs.map +1 -0
- package/dist/validate-BmyPbJCK.d.cts +1254 -0
- package/dist/validate-BmyPbJCK.d.cts.map +1 -0
- package/dist/validate-BmyPbJCK.d.mts +1254 -0
- package/dist/validate-BmyPbJCK.d.mts.map +1 -0
- package/package.json +84 -0
|
@@ -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.mts.map
|