@haddowg/json-api-client 1.0.0 → 1.2.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/dist/index.mjs CHANGED
@@ -15,6 +15,9 @@ var StructuralGuardError = class extends Error {
15
15
  /**
16
16
  * Thrown for any non-2xx JSON:API response. Carries the parsed error document plus
17
17
  * expressive status matchers and pointer-grouping helpers.
18
+ *
19
+ * `C` types each error object's `code` (see {@link JsonApiErrorObject}). It is a compile-time
20
+ * hint only: the runtime never checks a code against it.
18
21
  */
19
22
  var JsonApiError = class extends Error {
20
23
  status;
@@ -79,6 +82,60 @@ var JsonApiError = class extends Error {
79
82
  return out;
80
83
  }
81
84
  };
85
+ /**
86
+ * True when `err` is a {@link JsonApiError} and any of its error objects carries `code`. Takes
87
+ * any string, so it also matches codes the server never documented. A generated client exports a
88
+ * `hasErrorCode` bound to its server's `ErrorCode` union, which rejects a misspelt code at
89
+ * compile time; reach for this one only to match an undocumented code.
90
+ */
91
+ function hasErrorCode(err, code) {
92
+ return err instanceof JsonApiError && err.errors.some((e) => e.code === code);
93
+ }
94
+ //#endregion
95
+ //#region src/pagination.ts
96
+ /** The wire key for each role when the server keeps the default names. */
97
+ const CANONICAL_PAGE_KEYS = {
98
+ page: {
99
+ number: "number",
100
+ size: "size"
101
+ },
102
+ offset: {
103
+ offset: "offset",
104
+ limit: "limit"
105
+ },
106
+ cursor: {
107
+ after: "after",
108
+ before: "before",
109
+ size: "size"
110
+ },
111
+ fixed: { number: "number" },
112
+ none: {}
113
+ };
114
+ /**
115
+ * Resolve the strategy that answers a read from the endpoint's paginator and the `page` the
116
+ * request sends. A menu answers with the arm named by `page.kind`, else its `default`.
117
+ */
118
+ function resolvePaginator(paginator, page) {
119
+ if (paginator === void 0) return {
120
+ kind: "none",
121
+ keys: {}
122
+ };
123
+ if (typeof paginator === "string") return {
124
+ kind: paginator,
125
+ keys: CANONICAL_PAGE_KEYS[paginator]
126
+ };
127
+ if (paginator.strategy !== "multi") return {
128
+ kind: paginator.strategy,
129
+ keys: paginator.keys
130
+ };
131
+ const picked = page?.["kind"];
132
+ const arm = typeof picked === "string" && Object.hasOwn(paginator.arms, picked) ? picked : void 0;
133
+ const resolved = resolvePaginator(paginator.arms[arm ?? paginator.default]);
134
+ return arm === void 0 ? resolved : {
135
+ ...resolved,
136
+ arm
137
+ };
138
+ }
82
139
  //#endregion
83
140
  //#region src/validate.ts
84
141
  /** Whether a `validate` option is the schema-driven config (vs a bare validator function). */
@@ -191,9 +248,10 @@ const defineGetter = (target, key, get) => {
191
248
  * full resource) — a structural guess can't tell them apart for an attribute-less,
192
249
  * relation-less resource, so the caller passes the surface it issued.
193
250
  *
194
- * `paginatorOverride` sets the top-level array's `$page.kind` for a related/relationship read,
195
- * where the paginator is the RELATION's (which may diverge from the related type's collection —
196
- * D6a) and an empty page carries no member to sniff a type from. `primaryType` is preferred for a
251
+ * `paginatorOverride` sets the top-level array's `$page` for a related/relationship read, where the
252
+ * paginator is the RELATION's (which may diverge from the related type's collection — D6a), for a
253
+ * menu read that picked an arm, and where an empty page carries no member to sniff a type from. A
254
+ * bare kind is read with its canonical wire keys. `primaryType` is preferred for a
197
255
  * top-level collection read (statically-known); both override the sniff-from-`data[0]` fallback.
198
256
  */
199
257
  function materialise(doc, ctx, linkage = false, primaryType, paginatorOverride) {
@@ -246,7 +304,7 @@ function buildNode(raw, env, index, nodes, ctx) {
246
304
  for (const [name, rel] of Object.entries(rels)) {
247
305
  if (!("data" in rel)) continue;
248
306
  const relDesc = resType?.relations[name];
249
- node[name] = hydrateRelation(rel, relDesc?.cardinality, relatedPaginatorKind(relDesc, ctx), env, index, nodes, ctx);
307
+ node[name] = hydrateRelation(rel, relDesc?.cardinality, resolvePaginator(relatedPaginator(relDesc, ctx)), env, index, nodes, ctx);
250
308
  }
251
309
  attachResourceEnvelope(node, raw, env, rels);
252
310
  return node;
@@ -256,13 +314,13 @@ function buildNode(raw, env, index, nodes, ctx) {
256
314
  * linked-but-not-included member stays an identifier, an empty to-one is `null`, and a
257
315
  * to-many is always an augmented array (of views and/or identifiers).
258
316
  *
259
- * `relatedKind` is the related type's paginator kind (from the descriptor), threaded so a
260
- * to-many array reports the same `$page.kind` it would on its dedicated endpoint.
317
+ * `related` is the relation's paginator (from the descriptor), threaded so a to-many array
318
+ * reports the same `$page` it would on its dedicated endpoint without a `page` argument.
261
319
  */
262
- function hydrateRelation(rel, cardinality, relatedKind, env, index, nodes, ctx) {
320
+ function hydrateRelation(rel, cardinality, related, env, index, nodes, ctx) {
263
321
  const data = rel.data;
264
- if (Array.isArray(data)) return augmentRelationArray(data.map((member) => resolveMember(member, env, index, nodes, ctx)), rel, relatedKind, ctx);
265
- if (data === void 0 && cardinality === "many") return augmentRelationArray([], rel, relatedKind, ctx);
322
+ if (Array.isArray(data)) return augmentRelationArray(data.map((member) => resolveMember(member, env, index, nodes, ctx)), rel, related, ctx);
323
+ if (data === void 0 && cardinality === "many") return augmentRelationArray([], rel, related, ctx);
266
324
  if (data === null || data === void 0) return data === null ? null : void 0;
267
325
  if (isObject$2(data)) return resolveMember(data, env, index, nodes, ctx, {
268
326
  links: rel.links,
@@ -369,35 +427,40 @@ function augmentArray(members, doc, ctx, linkage, primaryType, paginatorOverride
369
427
  const links = doc.links;
370
428
  defineGetter(arr, "$links", () => links);
371
429
  defineGetter(arr, "$meta", () => doc.meta);
372
- attachPage(arr, paginatorOverride ?? paginatorKind(doc, ctx, primaryType), pageMeta(doc.meta), links, ctx, linkage);
430
+ attachPage(arr, paginatorOverride === void 0 ? resolvePaginator(primaryPaginator(doc, ctx, primaryType)) : typeof paginatorOverride === "string" ? resolvePaginator(paginatorOverride) : paginatorOverride, pageMeta(doc.meta), links, ctx, linkage);
373
431
  return arr;
374
432
  }
375
433
  /**
376
- * A to-many relationship array: `$page`/links/meta come from the relationship object. `kind`
377
- * is the related type's paginator (so an in-document compound-include to-many reports the same
434
+ * A to-many relationship array: `$page`/links/meta come from the relationship object.
435
+ * `paginator` is the relation's (so an in-document compound-include to-many reports the same
378
436
  * discriminant it would when fetched at its own relationship/related endpoint — "one model,
379
437
  * three surfaces").
380
438
  */
381
- function augmentRelationArray(members, rel, kind, ctx) {
439
+ function augmentRelationArray(members, rel, paginator, ctx) {
382
440
  const arr = [...members];
383
441
  const links = rel.links;
384
442
  defineGetter(arr, "$links", () => links);
385
443
  defineGetter(arr, "$meta", () => rel.meta);
386
- attachPage(arr, kind, pageMeta(rel.meta), links, ctx, false);
444
+ attachPage(arr, paginator, pageMeta(rel.meta), links, ctx, false);
387
445
  return arr;
388
446
  }
389
447
  /** Attach `$page` + `$next()`/`$prev()` driven by link presence (count-free-safe). */
390
- function attachPage(arr, kind, meta, links, ctx, linkage) {
448
+ function attachPage(arr, paginator, meta, links, ctx, linkage) {
391
449
  const page = {
392
- kind,
450
+ kind: paginator.kind,
451
+ keys: paginator.keys,
393
452
  links: pageLinks(links)
394
453
  };
454
+ if (paginator.arm !== void 0) page.arm = paginator.arm;
395
455
  if (meta !== void 0) page.meta = meta;
396
456
  defineGetter(arr, "$page", () => page);
397
- define(arr, "$next", () => navigateLink(page.links.next, ctx, linkage, kind));
398
- define(arr, "$prev", () => navigateLink(page.links.prev, ctx, linkage, kind));
457
+ define(arr, "$next", (options) => navigateLink(page.links.next, ctx, linkage, paginator, options?.signal));
458
+ define(arr, "$prev", (options) => navigateLink(page.links.prev, ctx, linkage, paginator, options?.signal));
399
459
  }
400
- const navigateLink = (link, ctx, linkage, paginator) => link === void 0 ? Promise.resolve(void 0) : ctx.navigate(link, linkage, paginator);
460
+ const navigateLink = (link, ctx, linkage, paginator, signal) => {
461
+ if (link === void 0) return Promise.resolve(void 0);
462
+ return signal === void 0 ? ctx.navigate(link, linkage, paginator) : ctx.navigate(link, linkage, paginator, signal);
463
+ };
401
464
  /** Extract the navigation links (a link may be a string or a `{href}` object). */
402
465
  function pageLinks(links) {
403
466
  const out = {};
@@ -419,18 +482,18 @@ const linkHref = (link) => {
419
482
  };
420
483
  const pageMeta = (meta) => isObject$2(meta?.["page"]) ? meta["page"] : void 0;
421
484
  /**
422
- * The document's paginator kind (from the primary type's descriptor; default `none`). Prefers the
423
- * statically-known `primaryType` (passed by the descriptor binding at the call site) so an EMPTY
424
- * page still reports the collection's real kind; falls back to sniffing the first member's type
425
- * (for a re-materialised `$next()` page or a related read, where the primary type isn't threaded).
485
+ * The document's paginator (from the primary type's descriptor). Prefers the statically-known
486
+ * `primaryType` (passed by the descriptor binding at the call site) so an EMPTY page still reports
487
+ * the collection's real kind; falls back to sniffing the first member's type (for a re-materialised
488
+ * `$next()` page or a related read, where the primary type isn't threaded).
426
489
  */
427
- function paginatorKind(doc, ctx, primaryType) {
490
+ function primaryPaginator(doc, ctx, primaryType) {
428
491
  const first = Array.isArray(doc.data) ? doc.data[0] : doc.data;
429
492
  const type = primaryType ?? (isObject$2(first) && typeof first["type"] === "string" ? first["type"] : void 0);
430
- return type !== void 0 ? ctx.descriptor[type]?.paginator ?? "none" : "none";
493
+ return type !== void 0 ? ctx.descriptor[type]?.paginator : void 0;
431
494
  }
432
495
  /**
433
- * A to-many relation's paginator kind. The relation's OWN related/relationship endpoint may
496
+ * A to-many relation's paginator. The relation's OWN related/relationship endpoint may
434
497
  * paginate differently than the related type's top-level collection (D6a), so prefer the
435
498
  * per-relation `paginator` (carried in the descriptor only when it diverges); otherwise fall back
436
499
  * to the related type's own paginator (the relationship and related endpoints both paginate that
@@ -438,11 +501,14 @@ function paginatorKind(doc, ctx, primaryType) {
438
501
  * resolves off its first declared related type (its members span types, matching the relationship
439
502
  * endpoint's first-member resolution).
440
503
  */
441
- function relatedPaginatorKind(relation, ctx) {
504
+ function relatedPaginator(relation, ctx) {
442
505
  if (relation?.paginator !== void 0) return relation.paginator;
443
506
  const relatedType = relation?.types[0];
444
- if (relatedType === void 0) return "none";
445
- return ctx.descriptor[relatedType]?.paginator ?? "none";
507
+ return relatedType === void 0 ? void 0 : ctx.descriptor[relatedType]?.paginator;
508
+ }
509
+ /** The kind a relation's related read answers with when it sends no `page`. See {@link relatedPaginator}. */
510
+ function relatedPaginatorKind(relation, ctx) {
511
+ return resolvePaginator(relatedPaginator(relation, ctx)).kind;
446
512
  }
447
513
  //#endregion
448
514
  //#region src/types.ts
@@ -472,6 +538,14 @@ const ATOMIC_MEDIA_TYPE = mediaType({ ext: [ATOMIC_EXT] });
472
538
  //#endregion
473
539
  //#region src/request.ts
474
540
  const isAbsolute = (path) => /^https?:\/\//i.test(path);
541
+ const RELATIVE_ORIGIN = "http://relative.invalid";
542
+ /** Resolve a server link against `baseUrl` per RFC 3986. An absolute link is kept verbatim. */
543
+ function resolveLink(link, baseUrl) {
544
+ if (isAbsolute(link)) return link;
545
+ if (isAbsolute(baseUrl)) return new URL(link, baseUrl).href;
546
+ const resolved = new URL(link, new URL(baseUrl, RELATIVE_ORIGIN));
547
+ return resolved.origin === RELATIVE_ORIGIN ? resolved.pathname + resolved.search + resolved.hash : resolved.href;
548
+ }
475
549
  const append = (parts, key, value) => {
476
550
  if (value === void 0 || value === null || value === "") return;
477
551
  parts.push(`${key}=${encodeURIComponent(String(value))}`);
@@ -508,14 +582,14 @@ function serializeQuery(query) {
508
582
  return parts.join("&");
509
583
  }
510
584
  /**
511
- * Execute a request: build the URL (verbatim when already absolute — for page links —
512
- * else `baseUrl + path`), apply content negotiation and any async per-request headers,
585
+ * Execute a request: build the URL (a server link resolved against `baseUrl`, an absolute path
586
+ * verbatim, else `baseUrl + path`), apply content negotiation and any async per-request headers,
513
587
  * then drive the transport. A 2xx with a body parses to a {@link Document}; an empty /
514
588
  * 204 body resolves to `undefined`. A non-2xx throws a {@link JsonApiError} carrying the
515
589
  * parsed error objects (tolerating a non-JSON error body with an empty list).
516
590
  */
517
591
  async function execute(ctx, req) {
518
- let url = isAbsolute(req.path) ? req.path : ctx.baseUrl + req.path;
592
+ let url = req.link ? resolveLink(req.path, ctx.baseUrl) : isAbsolute(req.path) ? req.path : ctx.baseUrl + req.path;
519
593
  if (req.query) {
520
594
  const qs = serializeQuery(req.query);
521
595
  if (qs) url += (url.includes("?") ? "&" : "?") + qs;
@@ -530,6 +604,7 @@ async function execute(ctx, req) {
530
604
  url,
531
605
  headers
532
606
  };
607
+ if (req.signal !== void 0) transportReq.signal = req.signal;
533
608
  if (req.body !== void 0) {
534
609
  headers["Content-Type"] = req.contentType ?? negotiated;
535
610
  transportReq.body = req.raw && typeof req.body === "string" ? req.body : JSON.stringify(req.body);
@@ -927,6 +1002,7 @@ const fetchTransport = async (req) => {
927
1002
  headers: req.headers
928
1003
  };
929
1004
  if (req.body !== void 0) init.body = req.body;
1005
+ if (req.signal !== void 0) init.signal = req.signal;
930
1006
  const res = await fetch(req.url, init);
931
1007
  const headers = {};
932
1008
  res.headers.forEach((value, key) => {
@@ -958,12 +1034,16 @@ async function run(ctx, type, operation, vars, query, linkage = false, relationF
958
1034
  path: fill(template, vars)
959
1035
  };
960
1036
  if (query !== void 0) {
961
- req.query = query;
962
- negotiateProfiles(ctx, type, query, req, relationForCount);
1037
+ const { signal, ...params } = query;
1038
+ req.query = params;
1039
+ if (signal !== void 0) req.signal = signal;
1040
+ negotiateProfiles(ctx, type, params, req, relationForCount);
963
1041
  }
964
1042
  const doc = await execute(ctx.request, req);
1043
+ const relation = (operation === "fetchRelated" || operation === "fetchRelationship") && vars["rel"] !== void 0 ? ctx.descriptor[type]?.relations[vars["rel"]] : void 0;
1044
+ const paginator = operation === "fetchMany" ? ctx.descriptor[type]?.paginator : relation !== void 0 ? relatedPaginator(relation, ctx.materialise) : void 0;
965
1045
  const primaryType = operation === "fetchMany" ? type : void 0;
966
- const paginatorOverride = (operation === "fetchRelated" || operation === "fetchRelationship") && vars["rel"] !== void 0 ? relatedPaginatorKind(ctx.descriptor[type]?.relations[vars["rel"]], ctx.materialise) : void 0;
1046
+ const paginatorOverride = paginator === void 0 ? void 0 : resolvePaginator(paginator, query?.page);
967
1047
  return doc === void 0 ? void 0 : materialise(doc, ctx.materialise, linkage, primaryType, paginatorOverride);
968
1048
  }
969
1049
  /**
@@ -1180,7 +1260,7 @@ function typeAccessor(ctx, type) {
1180
1260
  * not per-type codegen: `client.<type>` yields a {@link TypeAccessor}, `.id(id)` a handle,
1181
1261
  * and a handle's relation accessors drive the relationship/related endpoints. Navigation
1182
1262
  * (`$next()`/`$prev()`, related links) is wired through `materialise`'s `navigate` seam,
1183
- * which executes the (absolute) link and re-materialises.
1263
+ * which resolves the server's link against `baseUrl`, executes it and re-materialises.
1184
1264
  */
1185
1265
  function createClient(descriptor, options) {
1186
1266
  const request = {
@@ -1194,10 +1274,12 @@ function createClient(descriptor, options) {
1194
1274
  descriptor,
1195
1275
  materialise: {
1196
1276
  descriptor,
1197
- navigate: async (url, linkage = false, paginator) => {
1277
+ navigate: async (url, linkage = false, paginator, signal) => {
1198
1278
  const doc = await execute(request, {
1199
1279
  method: "GET",
1200
- path: url
1280
+ path: url,
1281
+ link: true,
1282
+ ...signal !== void 0 ? { signal } : {}
1201
1283
  });
1202
1284
  return doc === void 0 ? void 0 : materialise(doc, ctx.materialise, linkage, void 0, paginator);
1203
1285
  },
@@ -1227,6 +1309,6 @@ function createClient(descriptor, options) {
1227
1309
  });
1228
1310
  }
1229
1311
  //#endregion
1230
- export { ATOMIC_EXT, ATOMIC_MEDIA_TYPE, HANDLE_RESERVED, JSON_API_MEDIA_TYPE, JsonApiError, StructuralGuardError, assertJsonApiDocument, createClient, execute, fetchTransport, materialise, mediaType, relatedPaginatorKind, remapPointer, remapRelationshipPointer, resolveValidator, runAtomic, serializeQuery, toDocument, toRelationshipDocument, validateDocument, withRemappedAtomicPaths, withRemappedPaths, withRemappedRelationshipPaths };
1312
+ export { ATOMIC_EXT, ATOMIC_MEDIA_TYPE, CANONICAL_PAGE_KEYS, HANDLE_RESERVED, JSON_API_MEDIA_TYPE, JsonApiError, StructuralGuardError, assertJsonApiDocument, createClient, execute, fetchTransport, hasErrorCode, materialise, mediaType, relatedPaginator, relatedPaginatorKind, remapPointer, remapRelationshipPointer, resolvePaginator, resolveValidator, runAtomic, serializeQuery, toDocument, toRelationshipDocument, validateDocument, withRemappedAtomicPaths, withRemappedPaths, withRemappedRelationshipPaths };
1231
1313
 
1232
1314
  //# sourceMappingURL=index.mjs.map