@haddowg/json-api-client 1.3.1 → 1.4.1

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/README.md CHANGED
@@ -297,6 +297,12 @@ Retries are deliberately out of scope here — the transport (or the TanStack la
297
297
  or `$next({ signal })`). Forward it to your HTTP library so the request can be cancelled; the
298
298
  default `fetch` transport passes it to `fetch`.
299
299
 
300
+ Two optional members matter only for custom actions with non-JSON:API media types. Send
301
+ `req.rawBody` as is when it's set: a binary, `FormData` or `URLSearchParams` body in place of
302
+ `req.body`. When `req.responseType` is `'blob'` or `'arraybuffer'`, read a successful response
303
+ into `binary` as that type and leave `body` empty; read an error response as text in `body` as
304
+ usual. The `fetch` transport does both.
305
+
300
306
  ## Opt-in validation
301
307
 
302
308
  The core runtime trusts the wire (you own both ends; the envelope is invariant) and only runs the
package/dist/ajv.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as Validator, n as SchemaMap } from "./validate-B98ghVgw.cjs";
1
+ import { a as Validator, n as SchemaMap } from "./validate-CKioHWsK.cjs";
2
2
  import { ValidateFunction } from "ajv";
3
3
 
4
4
  //#region src/ajv.d.ts
package/dist/ajv.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as Validator, n as SchemaMap } from "./validate-B98ghVgw.mjs";
1
+ import { a as Validator, n as SchemaMap } from "./validate-CKioHWsK.mjs";
2
2
  import { ValidateFunction } from "ajv";
3
3
 
4
4
  //#region src/ajv.d.ts
package/dist/index.cjs CHANGED
@@ -570,7 +570,7 @@ const append = (parts, key, value) => {
570
570
  if (value === void 0 || value === null || value === "") return;
571
571
  parts.push(`${key}=${encodeURIComponent(String(value))}`);
572
572
  };
573
- const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
573
+ const isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
574
574
  /**
575
575
  * Append a filter value under its (already-bracketed) key. A plain object is a STRUCTURED filter
576
576
  * value — a `Range`/`DateRange` deepObject (`{ min, max }`) — and recurses into nested bracketed
@@ -579,7 +579,7 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
579
579
  * wire as `filter[k]=[object Object]` and be silently ignored by the server (D23).
580
580
  */
581
581
  const appendFilterValue = (parts, key, value) => {
582
- if (isPlainObject(value)) {
582
+ if (isPlainObject$1(value)) {
583
583
  for (const [subKey, subValue] of Object.entries(value)) appendFilterValue(parts, `${key}[${subKey}]`, subValue);
584
584
  return;
585
585
  }
@@ -609,6 +609,22 @@ function serializeQuery(query) {
609
609
  * parsed error objects (tolerating a non-JSON error body with an empty list).
610
610
  */
611
611
  async function execute(ctx, req) {
612
+ return parseDocument((await send(ctx, req)).body);
613
+ }
614
+ async function executeRaw(ctx, req, as) {
615
+ const res = await send(ctx, req, as === "text" ? void 0 : as);
616
+ if (as === "text") return res.body === "" ? void 0 : res.body;
617
+ if (res.status === 204) return;
618
+ if (res.binary === void 0) throw new Error("The transport returned no binary body for a request that asked for one; a custom transport must read the response as binary when the request sets responseType.");
619
+ return res.binary;
620
+ }
621
+ /**
622
+ * Build the URL (a server link resolved against `baseUrl` and held to its origin, an absolute
623
+ * path verbatim, else `baseUrl + path`), apply content negotiation and any async per-request
624
+ * headers, then drive the transport. A non-2xx throws a {@link JsonApiError} carrying the parsed
625
+ * error objects (tolerating a non-JSON error body with an empty list).
626
+ */
627
+ async function send(ctx, req, responseType) {
612
628
  let url;
613
629
  let sendHeaders = true;
614
630
  if (req.link) {
@@ -632,15 +648,23 @@ async function execute(ctx, req) {
632
648
  headers
633
649
  };
634
650
  if (req.signal !== void 0) transportReq.signal = req.signal;
635
- if (req.body !== void 0) {
636
- headers["Content-Type"] = req.contentType ?? negotiated;
637
- transportReq.body = req.raw && typeof req.body === "string" ? req.body : JSON.stringify(req.body);
651
+ if (responseType !== void 0) transportReq.responseType = responseType;
652
+ if (req.body !== void 0 || req.method.toUpperCase() === "POST") {
653
+ const contentType = req.contentType === void 0 ? negotiated : req.contentType;
654
+ if (contentType !== null) headers["Content-Type"] = contentType;
638
655
  }
656
+ if (typeof req.body === "string" && req.raw) transportReq.body = req.body;
657
+ else if (req.raw && isRawBody(req.body)) transportReq.rawBody = req.body;
658
+ else if (req.body !== void 0) transportReq.body = JSON.stringify(req.body);
639
659
  if (ctx.headers && sendHeaders) mergeHeaders(headers, await ctx.headers());
640
660
  const res = await ctx.transport(transportReq);
641
- if (res.status >= 200 && res.status < 300) return parseDocument(res.body);
661
+ if (res.status >= 200 && res.status < 300) return res;
642
662
  throw new JsonApiError(res.status, parseErrors(res.body));
643
663
  }
664
+ /** Whether a raw body goes to the transport as is rather than through `JSON.stringify`. */
665
+ function isRawBody(body) {
666
+ return typeof Blob !== "undefined" && body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || typeof FormData !== "undefined" && body instanceof FormData || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams;
667
+ }
644
668
  /**
645
669
  * Merge a `HeadersInit` (object / tuple array / `Headers`) into the outgoing map,
646
670
  * preserving the caller's casing (unlike the WHATWG `Headers`, which lowercases). The
@@ -1028,13 +1052,23 @@ const fetchTransport = async (req) => {
1028
1052
  method: req.method,
1029
1053
  headers: req.headers
1030
1054
  };
1031
- if (req.body !== void 0) init.body = req.body;
1055
+ if (req.rawBody !== void 0) init.body = req.rawBody;
1056
+ else if (req.body !== void 0) init.body = req.body;
1032
1057
  if (req.signal !== void 0) init.signal = req.signal;
1033
1058
  const res = await fetch(req.url, init);
1034
1059
  const headers = {};
1035
1060
  res.headers.forEach((value, key) => {
1036
1061
  headers[key] = value;
1037
1062
  });
1063
+ if (req.responseType !== void 0 && res.ok) {
1064
+ const binary = req.responseType === "blob" ? await res.blob() : await res.arrayBuffer();
1065
+ return {
1066
+ status: res.status,
1067
+ headers,
1068
+ body: "",
1069
+ binary
1070
+ };
1071
+ }
1038
1072
  return {
1039
1073
  status: res.status,
1040
1074
  headers,
@@ -1145,36 +1179,145 @@ async function runWrite(ctx, spec) {
1145
1179
  * - `raw` — the caller's payload, sent with the action's declared media type (e.g.
1146
1180
  * `application/octet-stream`), falling back to a wildcard when the spec declared none.
1147
1181
  *
1182
+ * An action listing several request media types (`inputMedia`) sends the one the caller picks
1183
+ * with `options.contentType`, or JSON:API when listed, else the first.
1184
+ *
1148
1185
  * The response is shaped by the action's `output`: `document` materialises into the resource /
1149
1186
  * collection view, `meta` returns the document's top-level `meta`, `none` resolves `undefined` (a
1150
- * `204` / no body).
1187
+ * `204` / no body). An action declaring `rawOutput` answers in the media type the caller picks
1188
+ * with `options.accept`: JSON:API as above, or a `string`, a `Blob` or `ArrayBuffer`, or parsed
1189
+ * JSON by the media type's kind.
1151
1190
  */
1152
- async function runAction(ctx, action, vars, input) {
1191
+ async function runAction(ctx, action, vars, input, options = {}) {
1153
1192
  const req = {
1154
1193
  method: action.method ?? "POST",
1155
1194
  path: fill(action.path, vars)
1156
1195
  };
1196
+ if (options.signal !== void 0) req.signal = options.signal;
1157
1197
  let remap;
1158
- if (action.input === "document") if (action.inputType !== void 0) {
1198
+ const media = requestMedia(action, options.contentType);
1199
+ if (media !== void 0 && media.kind !== "document") {
1200
+ req.body = encodeFields(media.mediaType, media.kind, input);
1201
+ req.raw = true;
1202
+ req.contentType = sentContentType(media.mediaType, media.kind, req.body);
1203
+ } else if (action.input === "document") if (action.inputType !== void 0) {
1159
1204
  const inputType = action.inputType;
1160
1205
  req.body = toDocument(ctx.descriptor, inputType, input ?? {});
1161
1206
  remap = (error) => withRemappedPaths(error, ctx.descriptor, inputType);
1162
1207
  } else req.body = input;
1163
1208
  else if (action.input === "raw") {
1164
- req.body = input;
1165
- req.contentType = action.contentType ?? "*/*";
1209
+ req.body = encodeFields(action.contentType ?? "*/*", void 0, input);
1210
+ req.contentType = sentContentType(action.contentType ?? "*/*", void 0, req.body);
1166
1211
  req.raw = true;
1167
1212
  }
1168
- let doc;
1213
+ const raw = responseMedia(action, options.accept);
1169
1214
  try {
1170
- doc = await execute(ctx.request, req);
1215
+ if (raw !== void 0) {
1216
+ req.accept = raw.mediaType;
1217
+ if (raw.kind === "binary") return await executeRaw(ctx.request, req, options.responseType === "arrayBuffer" ? "arraybuffer" : "blob");
1218
+ const text = await executeRaw(ctx.request, req, "text");
1219
+ return raw.kind === "json" && text !== void 0 ? JSON.parse(text) : text;
1220
+ }
1221
+ const doc = await execute(ctx.request, req);
1222
+ if (action.output === "none" || doc === void 0) return;
1223
+ if (action.output === "meta") return doc.meta;
1224
+ return materialise(doc, ctx.materialise);
1171
1225
  } catch (error) {
1172
1226
  if (remap !== void 0 && error instanceof JsonApiError) throw remap(error);
1173
1227
  throw error;
1174
1228
  }
1175
- if (action.output === "none" || doc === void 0) return;
1176
- if (action.output === "meta") return doc.meta;
1177
- return materialise(doc, ctx.materialise);
1229
+ }
1230
+ /**
1231
+ * The request media type an action with several (`inputMedia`) sends: the caller's pick, matched
1232
+ * exactly or through a declared wildcard such as `image/*`, else JSON:API when listed, else the
1233
+ * first. `undefined` for an action with a single request media type.
1234
+ */
1235
+ function requestMedia(action, picked) {
1236
+ const listed = action.inputMedia;
1237
+ if (listed === void 0 || listed.length === 0) {
1238
+ if (picked !== void 0) throw new Error(`This action declares no request media types to choose "${picked}" from.`);
1239
+ return;
1240
+ }
1241
+ if (picked === void 0) {
1242
+ const media = listed.find((entry) => entry.mediaType === "application/vnd.api+json") ?? listed[0];
1243
+ return {
1244
+ mediaType: media.mediaType,
1245
+ kind: media.kind
1246
+ };
1247
+ }
1248
+ const essence = picked.split(";")[0].trim().toLowerCase();
1249
+ const match = listed.find((entry) => entry.mediaType === essence) ?? listed.find((entry) => entry.mediaType === `${essence.split("/")[0]}/*`) ?? listed.find((entry) => entry.mediaType === "*/*");
1250
+ if (match === void 0) throw new Error(`This action does not accept "${picked}"; it accepts ${listed.map((entry) => entry.mediaType).join(", ")}.`);
1251
+ return {
1252
+ mediaType: picked,
1253
+ kind: match.kind
1254
+ };
1255
+ }
1256
+ /**
1257
+ * A multipart or URL-encoded body given as a plain object of fields, encoded into a `FormData` or
1258
+ * `URLSearchParams`. Any other body, a ready `FormData` included, is sent as it is.
1259
+ */
1260
+ function encodeFields(mediaType, kind, body) {
1261
+ const multipart = kind === "multipart" || mediaType.startsWith("multipart/");
1262
+ if (!multipart && !(kind === "form" || mediaType === "application/x-www-form-urlencoded") || !isPlainObject(body)) return body;
1263
+ return multipart ? toFormData(body) : toSearchParams(body);
1264
+ }
1265
+ /** Whether a body is a plain object of fields rather than a ready payload. */
1266
+ function isPlainObject(value) {
1267
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1268
+ const proto = Object.getPrototypeOf(value);
1269
+ return proto === Object.prototype || proto === null;
1270
+ }
1271
+ /**
1272
+ * The entries a field contributes: nothing for `null` or `undefined`, one `name[]` entry per item
1273
+ * for an array (the bracket suffix is how PHP servers read a repeated field as a list), else one
1274
+ * `name` entry.
1275
+ */
1276
+ function fieldEntries(name, value) {
1277
+ if (value === null || value === void 0) return [];
1278
+ if (Array.isArray(value)) return value.filter((item) => item !== null && item !== void 0).map((item) => [`${name}[]`, item]);
1279
+ return [[name, value]];
1280
+ }
1281
+ /** A field's text value: booleans as `true` or `false`, everything else through `String`. */
1282
+ function fieldText(value) {
1283
+ return typeof value === "boolean" ? value ? "true" : "false" : String(value);
1284
+ }
1285
+ /** A multipart body from fields. A `File` keeps its name; a bare `Blob` is named after its field. */
1286
+ function toFormData(fields) {
1287
+ const data = new FormData();
1288
+ for (const [field, value] of Object.entries(fields)) for (const [name, item] of fieldEntries(field, value)) if (typeof Blob !== "undefined" && item instanceof Blob) {
1289
+ const fileName = typeof File !== "undefined" && item instanceof File && item.name !== "" ? item.name : field;
1290
+ data.append(name, item, fileName);
1291
+ } else data.append(name, fieldText(item));
1292
+ return data;
1293
+ }
1294
+ /** A URL-encoded body from fields. */
1295
+ function toSearchParams(fields) {
1296
+ const params = new URLSearchParams();
1297
+ for (const [field, value] of Object.entries(fields)) for (const [name, item] of fieldEntries(field, value)) params.append(name, fieldText(item));
1298
+ return params;
1299
+ }
1300
+ /**
1301
+ * The `Content-Type` for a non-JSON:API body. A multipart `FormData` body gets none, so the
1302
+ * transport writes the boundary. A wildcard media type takes a `Blob` body's own type.
1303
+ */
1304
+ function sentContentType(mediaType, kind, body) {
1305
+ if ((kind === "multipart" || mediaType.startsWith("multipart/")) && typeof FormData !== "undefined" && body instanceof FormData) return null;
1306
+ if (mediaType.includes("*") && typeof Blob !== "undefined" && body instanceof Blob) return body.type === "" ? mediaType : body.type;
1307
+ return mediaType;
1308
+ }
1309
+ /**
1310
+ * The non-JSON:API media type an action is asked to answer in: the caller's `accept` when it isn't
1311
+ * JSON:API, else, with no `accept`, the first raw media type of an action that answers in no
1312
+ * JSON:API document. `undefined` means a JSON:API response.
1313
+ */
1314
+ function responseMedia(action, accept) {
1315
+ const raw = action.rawOutput ?? [];
1316
+ if (accept === void 0) return action.output === "none" ? raw[0] : void 0;
1317
+ if (accept === "application/vnd.api+json") return;
1318
+ const media = raw.find((entry) => entry.mediaType === accept);
1319
+ if (media === void 0) throw new Error(`This action does not answer in "${accept}"; it answers in ${[...action.output === "none" ? [] : [JSON_API_MEDIA_TYPE], ...raw.map((entry) => entry.mediaType)].join(", ")}.`);
1320
+ return media;
1178
1321
  }
1179
1322
  /**
1180
1323
  * Build the typed `.actions` accessor at one scope: a plain object exposing exactly the
@@ -1186,7 +1329,7 @@ async function runAction(ctx, action, vars, input) {
1186
1329
  function actionsAccessor(ctx, type, scope, vars) {
1187
1330
  const actions = ctx.descriptor[type]?.actions ?? {};
1188
1331
  const out = {};
1189
- for (const [name, action] of Object.entries(actions)) if (action.scope === scope) out[name] = (input) => runAction(ctx, action, vars, input);
1332
+ for (const [name, action] of Object.entries(actions)) if (action.scope === scope) out[name] = (...args) => action.input === "none" ? runAction(ctx, action, vars, void 0, args[0]) : runAction(ctx, action, vars, args[0], args[1]);
1190
1333
  return out;
1191
1334
  }
1192
1335
  /**
@@ -1360,6 +1503,7 @@ exports.StructuralGuardError = StructuralGuardError;
1360
1503
  exports.assertJsonApiDocument = assertJsonApiDocument;
1361
1504
  exports.createClient = createClient;
1362
1505
  exports.execute = execute;
1506
+ exports.executeRaw = executeRaw;
1363
1507
  exports.fetchTransport = fetchTransport;
1364
1508
  exports.hasErrorCode = hasErrorCode;
1365
1509
  exports.materialise = materialise;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as RelationshipAccessor, $t as ActionDescriptor, A as DefaultActionTypes, At as DocumentEnvelope, B as IdentifierMember, Bt as relatedPaginatorKind, C as fetchTransport, Ct as ATOMIC_MEDIA_TYPE, D as Collection, Dt as ResourceIdentifier, E as Client, Et as MediaTypeParams, F as FieldSelectionMap, Ft as RelationView, G as MemberName, Gt as PageLinks, H as IncludedRelations, Ht as ExactPageParams, I as FieldsMap, It as ResourceEnvelope, J as ReadableRelationName, Jt as PageParams, K as PivotInput, Kt as PageMeta, L as FilterKey, Lt as ResourceLinks, M as DefaultWriteAttributes, Mt as EdgeEnvelope, N as EdgeMembers, Nt as MaterialiseContext, O as CountToken, Ot as mediaType, P as ErrorCodeOf, Pt as NavigateOptions, Q as RelationReadQuery, Qt as resolvePaginator, R as FilterQuery, Rt as materialise, S as TransportResponse, St as ATOMIC_EXT, T as AtomicResultOf, Tt as LocalIdentifier, U as LinkageRef, Ut as Page, V as IncludePath, Vt as CANONICAL_PAGE_KEYS, W as ListPageParams, Wt as PageKeys, X as RelationPageParams, Xt as ResolvedPaginator, Y as RelationName, Yt as PageParamsOf, Z as RelationPaginator, Zt as TypedPage, _ as HANDLE_RESERVED, _n as StrategyDescriptor, _t as AtomicResult, a as Validator, an as Cardinality, at as SortToken, b as JsonApiTransport, bt as AtomicUpdateInput, c as resolveValidator, cn as MenuDescriptor, ct as TypedReadQuery, d as JsonApiContext, dn as PaginatorRole, dt as AtomicCreateHandle, en as ActionInput, et as ResourceAccessors, f as JsonApiRequest, fn as PaginatorStrategy, ft as AtomicCreateInput, g as ClientOptions, gn as ResourceDescriptor, gt as AtomicRef, h as serializeQuery, hn as RelationMutations, ht as AtomicRecorder, i as ValidationOption, in as AtomicDescriptor, it as SortQuery, j as DefaultAttributes, jt as Edge, k as CreateInput, kt as ArrayEnvelope, l as validateDocument, ln as PaginatorDescriptor, lt as UpdateInput, m as execute, mn as RelationDescriptor, mt as AtomicHandle, n as SchemaMap, nn as ActionScope, nt as ResourceObjectView, o as WireResource, on as ClientIdPolicy, ot as TypeAccessor, p as ReadQuery, pn as PivotDescriptor, pt as AtomicDeleteHandle, q as ReadResult, qt as PageOf, r as ValidationConfig, rn as ApiDescriptor, rt as SingleReadQuery, s as assertJsonApiDocument, sn as CountableDescriptor, st as TypeName, t as ResolvedValidator, tn as ActionOutput, tt as ResourceHandle, u as Document, un as PaginatorKind, ut as WriteOptions, v as HeadersInit, vt as AtomicResults, w as ActionsAccessor, wt as JSON_API_MEDIA_TYPE, x as TransportRequest, xt as runAtomic, y as createClient, yt as AtomicUpdateHandle, z as HydratedMember, zt as relatedPaginator } from "./validate-B98ghVgw.cjs";
1
+ import { $ as ReadResult, $t as PageOf, A as BinaryBody, At as JSON_API_MEDIA_TYPE, B as ErrorCodeOf, Bt as NavigateOptions, C as TransportRawBody, Cn as RawOutputMedia, Ct as AtomicResult, D as ActionCallOptions, Dn as StrategyDescriptor, Dt as runAtomic, E as fetchTransport, En as ResourceDescriptor, Et as AtomicUpdateInput, F as CreateInput, Ft as ArrayEnvelope, G as HydratedMember, Gt as relatedPaginator, H as FieldsMap, Ht as ResourceEnvelope, I as DefaultActionTypes, It as DocumentEnvelope, J as IncludedRelations, Jt as ExactPageParams, K as IdentifierMember, Kt as relatedPaginatorKind, L as DefaultAttributes, Lt as Edge, M as Client, Mt as MediaTypeParams, N as Collection, Nt as ResourceIdentifier, O as ActionsAccessor, Ot as ATOMIC_EXT, P as CountToken, Pt as mediaType, Q as PivotInput, Qt as PageMeta, R as DefaultWriteAttributes, Rt as EdgeEnvelope, S as JsonApiTransport, Sn as RawMediaKind, St as AtomicRef, T as TransportResponse, Tn as RelationMutations, Tt as AtomicUpdateHandle, U as FilterKey, Ut as ResourceLinks, V as FieldSelectionMap, Vt as RelationView, W as FilterQuery, Wt as materialise, X as ListPageParams, Xt as PageKeys, Y as LinkageRef, Yt as Page, Z as MemberName, Zt as PageLinks, _ as serializeQuery, _n as PaginatorDescriptor, _t as AtomicCreateHandle, a as Validator, an as ActionDescriptor, at as RelationshipAccessor, b as HeadersInit, bn as PaginatorStrategy, bt as AtomicHandle, c as resolveValidator, cn as ActionScope, ct as ResourceObjectView, d as JsonApiContext, dn as Cardinality, dt as SortToken, en as PageParams, et as ReadableRelationName, f as JsonApiRequest, fn as ClientIdPolicy, ft as TypeAccessor, g as executeRaw, gn as MenuDescriptor, gt as WriteOptions, h as execute, hn as InputMediaKind, ht as UpdateInput, i as ValidationOption, in as resolvePaginator, it as RelationReadQuery, j as BinaryResponseType, jt as LocalIdentifier, k as AtomicResultOf, kt as ATOMIC_MEDIA_TYPE, l as validateDocument, ln as ApiDescriptor, lt as SingleReadQuery, m as ReadQuery, mn as InputMedia, mt as TypedReadQuery, n as SchemaMap, nn as ResolvedPaginator, nt as RelationPageParams, o as WireResource, on as ActionInput, ot as ResourceAccessors, p as RawResponseType, pn as CountableDescriptor, pt as TypeName, q as IncludePath, qt as CANONICAL_PAGE_KEYS, r as ValidationConfig, rn as TypedPage, rt as RelationPaginator, s as assertJsonApiDocument, sn as ActionOutput, st as ResourceHandle, t as ResolvedValidator, tn as PageParamsOf, tt as RelationName, u as Document, un as AtomicDescriptor, ut as SortQuery, v as ClientOptions, vn as PaginatorKind, vt as AtomicCreateInput, w as TransportRequest, wn as RelationDescriptor, wt as AtomicResults, x as createClient, xn as PivotDescriptor, xt as AtomicRecorder, y as HANDLE_RESERVED, yn as PaginatorRole, yt as AtomicDeleteHandle, z as EdgeMembers, zt as MaterialiseContext } from "./validate-CKioHWsK.cjs";
2
2
 
3
3
  //#region src/errors.d.ts
4
4
  /**
@@ -216,5 +216,5 @@ declare function withRemappedRelationshipPaths(error: JsonApiError, rel: string,
216
216
  */
217
217
  declare function withRemappedAtomicPaths(error: JsonApiError, descriptor: ApiDescriptor, typeForOp: (opIndex: number) => string | undefined): JsonApiError;
218
218
  //#endregion
219
- export { ATOMIC_EXT, ATOMIC_MEDIA_TYPE, ActionDescriptor, ActionInput, ActionOutput, ActionScope, ActionsAccessor, ApiDescriptor, ArrayEnvelope, AtomicCreateHandle, AtomicCreateInput, AtomicDeleteHandle, AtomicDescriptor, AtomicHandle, AtomicRecorder, AtomicRef, AtomicResult, AtomicResultOf, AtomicResults, AtomicUpdateHandle, AtomicUpdateInput, CANONICAL_PAGE_KEYS, Cardinality, Client, ClientIdPolicy, ClientOptions, Collection, CountToken, CountableDescriptor, CreateInput, DefaultActionTypes, DefaultAttributes, DefaultWriteAttributes, Document, DocumentEnvelope, Edge, EdgeEnvelope, EdgeMembers, ErrorCodeGuard, ErrorCodeOf, ExactPageParams, FieldSelectionMap, FieldsMap, FilterKey, FilterQuery, HANDLE_RESERVED, HeadersInit, HydratedMember, IdentifierMember, IncludePath, IncludedRelations, JSON_API_MEDIA_TYPE, JsonApiContext, JsonApiError, JsonApiErrorObject, JsonApiRequest, JsonApiTransport, LinkageRef, ListPageParams, LocalIdentifier, MaterialiseContext, MediaTypeParams, MemberName, MenuDescriptor, NavigateOptions, Page, PageKeys, PageLinks, PageMeta, PageOf, PageParams, PageParamsOf, PaginatorDescriptor, PaginatorKind, PaginatorRole, PaginatorStrategy, PivotDescriptor, PivotInput, ReadQuery, ReadResult, ReadableRelationName, RelationDescriptor, RelationMutations, RelationName, RelationPageParams, RelationPaginator, RelationReadQuery, RelationView, RelationshipAccessor, RelationshipDocument, ResolvedPaginator, ResolvedValidator, ResourceAccessors, ResourceDescriptor, ResourceEnvelope, ResourceHandle, ResourceIdentifier, ResourceLinks, ResourceObjectView, SchemaMap, SingleReadQuery, SortQuery, SortToken, StrategyDescriptor, StructuralGuardError, ToDocumentOptions, TransportRequest, TransportResponse, TypeAccessor, TypeName, TypedPage, TypedReadQuery, UpdateInput, ValidationConfig, ValidationOption, Validator, WireResource, WriteDocument, WriteOptions, assertJsonApiDocument, createClient, execute, fetchTransport, hasErrorCode, materialise, mediaType, relatedPaginator, relatedPaginatorKind, remapPointer, remapRelationshipPointer, resolvePaginator, resolveValidator, runAtomic, serializeQuery, toDocument, toRelationshipDocument, validateDocument, withRemappedAtomicPaths, withRemappedPaths, withRemappedRelationshipPaths };
219
+ export { ATOMIC_EXT, ATOMIC_MEDIA_TYPE, ActionCallOptions, ActionDescriptor, ActionInput, ActionOutput, ActionScope, ActionsAccessor, ApiDescriptor, ArrayEnvelope, AtomicCreateHandle, AtomicCreateInput, AtomicDeleteHandle, AtomicDescriptor, AtomicHandle, AtomicRecorder, AtomicRef, AtomicResult, AtomicResultOf, AtomicResults, AtomicUpdateHandle, AtomicUpdateInput, BinaryBody, BinaryResponseType, CANONICAL_PAGE_KEYS, Cardinality, Client, ClientIdPolicy, ClientOptions, Collection, CountToken, CountableDescriptor, CreateInput, DefaultActionTypes, DefaultAttributes, DefaultWriteAttributes, Document, DocumentEnvelope, Edge, EdgeEnvelope, EdgeMembers, ErrorCodeGuard, ErrorCodeOf, ExactPageParams, FieldSelectionMap, FieldsMap, FilterKey, FilterQuery, HANDLE_RESERVED, HeadersInit, HydratedMember, IdentifierMember, IncludePath, IncludedRelations, InputMedia, InputMediaKind, JSON_API_MEDIA_TYPE, JsonApiContext, JsonApiError, JsonApiErrorObject, JsonApiRequest, JsonApiTransport, LinkageRef, ListPageParams, LocalIdentifier, MaterialiseContext, MediaTypeParams, MemberName, MenuDescriptor, NavigateOptions, Page, PageKeys, PageLinks, PageMeta, PageOf, PageParams, PageParamsOf, PaginatorDescriptor, PaginatorKind, PaginatorRole, PaginatorStrategy, PivotDescriptor, PivotInput, RawMediaKind, RawOutputMedia, RawResponseType, ReadQuery, ReadResult, ReadableRelationName, RelationDescriptor, RelationMutations, RelationName, RelationPageParams, RelationPaginator, RelationReadQuery, RelationView, RelationshipAccessor, RelationshipDocument, ResolvedPaginator, ResolvedValidator, ResourceAccessors, ResourceDescriptor, ResourceEnvelope, ResourceHandle, ResourceIdentifier, ResourceLinks, ResourceObjectView, SchemaMap, SingleReadQuery, SortQuery, SortToken, StrategyDescriptor, StructuralGuardError, ToDocumentOptions, TransportRawBody, TransportRequest, TransportResponse, TypeAccessor, TypeName, TypedPage, TypedReadQuery, UpdateInput, ValidationConfig, ValidationOption, Validator, WireResource, WriteDocument, WriteOptions, assertJsonApiDocument, createClient, execute, executeRaw, fetchTransport, hasErrorCode, materialise, mediaType, relatedPaginator, relatedPaginatorKind, remapPointer, remapRelationshipPointer, resolvePaginator, resolveValidator, runAtomic, serializeQuery, toDocument, toRelationshipDocument, validateDocument, withRemappedAtomicPaths, withRemappedPaths, withRemappedRelationshipPaths };
220
220
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as RelationshipAccessor, $t as ActionDescriptor, A as DefaultActionTypes, At as DocumentEnvelope, B as IdentifierMember, Bt as relatedPaginatorKind, C as fetchTransport, Ct as ATOMIC_MEDIA_TYPE, D as Collection, Dt as ResourceIdentifier, E as Client, Et as MediaTypeParams, F as FieldSelectionMap, Ft as RelationView, G as MemberName, Gt as PageLinks, H as IncludedRelations, Ht as ExactPageParams, I as FieldsMap, It as ResourceEnvelope, J as ReadableRelationName, Jt as PageParams, K as PivotInput, Kt as PageMeta, L as FilterKey, Lt as ResourceLinks, M as DefaultWriteAttributes, Mt as EdgeEnvelope, N as EdgeMembers, Nt as MaterialiseContext, O as CountToken, Ot as mediaType, P as ErrorCodeOf, Pt as NavigateOptions, Q as RelationReadQuery, Qt as resolvePaginator, R as FilterQuery, Rt as materialise, S as TransportResponse, St as ATOMIC_EXT, T as AtomicResultOf, Tt as LocalIdentifier, U as LinkageRef, Ut as Page, V as IncludePath, Vt as CANONICAL_PAGE_KEYS, W as ListPageParams, Wt as PageKeys, X as RelationPageParams, Xt as ResolvedPaginator, Y as RelationName, Yt as PageParamsOf, Z as RelationPaginator, Zt as TypedPage, _ as HANDLE_RESERVED, _n as StrategyDescriptor, _t as AtomicResult, a as Validator, an as Cardinality, at as SortToken, b as JsonApiTransport, bt as AtomicUpdateInput, c as resolveValidator, cn as MenuDescriptor, ct as TypedReadQuery, d as JsonApiContext, dn as PaginatorRole, dt as AtomicCreateHandle, en as ActionInput, et as ResourceAccessors, f as JsonApiRequest, fn as PaginatorStrategy, ft as AtomicCreateInput, g as ClientOptions, gn as ResourceDescriptor, gt as AtomicRef, h as serializeQuery, hn as RelationMutations, ht as AtomicRecorder, i as ValidationOption, in as AtomicDescriptor, it as SortQuery, j as DefaultAttributes, jt as Edge, k as CreateInput, kt as ArrayEnvelope, l as validateDocument, ln as PaginatorDescriptor, lt as UpdateInput, m as execute, mn as RelationDescriptor, mt as AtomicHandle, n as SchemaMap, nn as ActionScope, nt as ResourceObjectView, o as WireResource, on as ClientIdPolicy, ot as TypeAccessor, p as ReadQuery, pn as PivotDescriptor, pt as AtomicDeleteHandle, q as ReadResult, qt as PageOf, r as ValidationConfig, rn as ApiDescriptor, rt as SingleReadQuery, s as assertJsonApiDocument, sn as CountableDescriptor, st as TypeName, t as ResolvedValidator, tn as ActionOutput, tt as ResourceHandle, u as Document, un as PaginatorKind, ut as WriteOptions, v as HeadersInit, vt as AtomicResults, w as ActionsAccessor, wt as JSON_API_MEDIA_TYPE, x as TransportRequest, xt as runAtomic, y as createClient, yt as AtomicUpdateHandle, z as HydratedMember, zt as relatedPaginator } from "./validate-B98ghVgw.mjs";
1
+ import { $ as ReadResult, $t as PageOf, A as BinaryBody, At as JSON_API_MEDIA_TYPE, B as ErrorCodeOf, Bt as NavigateOptions, C as TransportRawBody, Cn as RawOutputMedia, Ct as AtomicResult, D as ActionCallOptions, Dn as StrategyDescriptor, Dt as runAtomic, E as fetchTransport, En as ResourceDescriptor, Et as AtomicUpdateInput, F as CreateInput, Ft as ArrayEnvelope, G as HydratedMember, Gt as relatedPaginator, H as FieldsMap, Ht as ResourceEnvelope, I as DefaultActionTypes, It as DocumentEnvelope, J as IncludedRelations, Jt as ExactPageParams, K as IdentifierMember, Kt as relatedPaginatorKind, L as DefaultAttributes, Lt as Edge, M as Client, Mt as MediaTypeParams, N as Collection, Nt as ResourceIdentifier, O as ActionsAccessor, Ot as ATOMIC_EXT, P as CountToken, Pt as mediaType, Q as PivotInput, Qt as PageMeta, R as DefaultWriteAttributes, Rt as EdgeEnvelope, S as JsonApiTransport, Sn as RawMediaKind, St as AtomicRef, T as TransportResponse, Tn as RelationMutations, Tt as AtomicUpdateHandle, U as FilterKey, Ut as ResourceLinks, V as FieldSelectionMap, Vt as RelationView, W as FilterQuery, Wt as materialise, X as ListPageParams, Xt as PageKeys, Y as LinkageRef, Yt as Page, Z as MemberName, Zt as PageLinks, _ as serializeQuery, _n as PaginatorDescriptor, _t as AtomicCreateHandle, a as Validator, an as ActionDescriptor, at as RelationshipAccessor, b as HeadersInit, bn as PaginatorStrategy, bt as AtomicHandle, c as resolveValidator, cn as ActionScope, ct as ResourceObjectView, d as JsonApiContext, dn as Cardinality, dt as SortToken, en as PageParams, et as ReadableRelationName, f as JsonApiRequest, fn as ClientIdPolicy, ft as TypeAccessor, g as executeRaw, gn as MenuDescriptor, gt as WriteOptions, h as execute, hn as InputMediaKind, ht as UpdateInput, i as ValidationOption, in as resolvePaginator, it as RelationReadQuery, j as BinaryResponseType, jt as LocalIdentifier, k as AtomicResultOf, kt as ATOMIC_MEDIA_TYPE, l as validateDocument, ln as ApiDescriptor, lt as SingleReadQuery, m as ReadQuery, mn as InputMedia, mt as TypedReadQuery, n as SchemaMap, nn as ResolvedPaginator, nt as RelationPageParams, o as WireResource, on as ActionInput, ot as ResourceAccessors, p as RawResponseType, pn as CountableDescriptor, pt as TypeName, q as IncludePath, qt as CANONICAL_PAGE_KEYS, r as ValidationConfig, rn as TypedPage, rt as RelationPaginator, s as assertJsonApiDocument, sn as ActionOutput, st as ResourceHandle, t as ResolvedValidator, tn as PageParamsOf, tt as RelationName, u as Document, un as AtomicDescriptor, ut as SortQuery, v as ClientOptions, vn as PaginatorKind, vt as AtomicCreateInput, w as TransportRequest, wn as RelationDescriptor, wt as AtomicResults, x as createClient, xn as PivotDescriptor, xt as AtomicRecorder, y as HANDLE_RESERVED, yn as PaginatorRole, yt as AtomicDeleteHandle, z as EdgeMembers, zt as MaterialiseContext } from "./validate-CKioHWsK.mjs";
2
2
 
3
3
  //#region src/errors.d.ts
4
4
  /**
@@ -216,5 +216,5 @@ declare function withRemappedRelationshipPaths(error: JsonApiError, rel: string,
216
216
  */
217
217
  declare function withRemappedAtomicPaths(error: JsonApiError, descriptor: ApiDescriptor, typeForOp: (opIndex: number) => string | undefined): JsonApiError;
218
218
  //#endregion
219
- export { ATOMIC_EXT, ATOMIC_MEDIA_TYPE, ActionDescriptor, ActionInput, ActionOutput, ActionScope, ActionsAccessor, ApiDescriptor, ArrayEnvelope, AtomicCreateHandle, AtomicCreateInput, AtomicDeleteHandle, AtomicDescriptor, AtomicHandle, AtomicRecorder, AtomicRef, AtomicResult, AtomicResultOf, AtomicResults, AtomicUpdateHandle, AtomicUpdateInput, CANONICAL_PAGE_KEYS, Cardinality, Client, ClientIdPolicy, ClientOptions, Collection, CountToken, CountableDescriptor, CreateInput, DefaultActionTypes, DefaultAttributes, DefaultWriteAttributes, Document, DocumentEnvelope, Edge, EdgeEnvelope, EdgeMembers, ErrorCodeGuard, ErrorCodeOf, ExactPageParams, FieldSelectionMap, FieldsMap, FilterKey, FilterQuery, HANDLE_RESERVED, HeadersInit, HydratedMember, IdentifierMember, IncludePath, IncludedRelations, JSON_API_MEDIA_TYPE, JsonApiContext, JsonApiError, JsonApiErrorObject, JsonApiRequest, JsonApiTransport, LinkageRef, ListPageParams, LocalIdentifier, MaterialiseContext, MediaTypeParams, MemberName, MenuDescriptor, NavigateOptions, Page, PageKeys, PageLinks, PageMeta, PageOf, PageParams, PageParamsOf, PaginatorDescriptor, PaginatorKind, PaginatorRole, PaginatorStrategy, PivotDescriptor, PivotInput, ReadQuery, ReadResult, ReadableRelationName, RelationDescriptor, RelationMutations, RelationName, RelationPageParams, RelationPaginator, RelationReadQuery, RelationView, RelationshipAccessor, RelationshipDocument, ResolvedPaginator, ResolvedValidator, ResourceAccessors, ResourceDescriptor, ResourceEnvelope, ResourceHandle, ResourceIdentifier, ResourceLinks, ResourceObjectView, SchemaMap, SingleReadQuery, SortQuery, SortToken, StrategyDescriptor, StructuralGuardError, ToDocumentOptions, TransportRequest, TransportResponse, TypeAccessor, TypeName, TypedPage, TypedReadQuery, UpdateInput, ValidationConfig, ValidationOption, Validator, WireResource, WriteDocument, WriteOptions, assertJsonApiDocument, createClient, execute, fetchTransport, hasErrorCode, materialise, mediaType, relatedPaginator, relatedPaginatorKind, remapPointer, remapRelationshipPointer, resolvePaginator, resolveValidator, runAtomic, serializeQuery, toDocument, toRelationshipDocument, validateDocument, withRemappedAtomicPaths, withRemappedPaths, withRemappedRelationshipPaths };
219
+ export { ATOMIC_EXT, ATOMIC_MEDIA_TYPE, ActionCallOptions, ActionDescriptor, ActionInput, ActionOutput, ActionScope, ActionsAccessor, ApiDescriptor, ArrayEnvelope, AtomicCreateHandle, AtomicCreateInput, AtomicDeleteHandle, AtomicDescriptor, AtomicHandle, AtomicRecorder, AtomicRef, AtomicResult, AtomicResultOf, AtomicResults, AtomicUpdateHandle, AtomicUpdateInput, BinaryBody, BinaryResponseType, CANONICAL_PAGE_KEYS, Cardinality, Client, ClientIdPolicy, ClientOptions, Collection, CountToken, CountableDescriptor, CreateInput, DefaultActionTypes, DefaultAttributes, DefaultWriteAttributes, Document, DocumentEnvelope, Edge, EdgeEnvelope, EdgeMembers, ErrorCodeGuard, ErrorCodeOf, ExactPageParams, FieldSelectionMap, FieldsMap, FilterKey, FilterQuery, HANDLE_RESERVED, HeadersInit, HydratedMember, IdentifierMember, IncludePath, IncludedRelations, InputMedia, InputMediaKind, JSON_API_MEDIA_TYPE, JsonApiContext, JsonApiError, JsonApiErrorObject, JsonApiRequest, JsonApiTransport, LinkageRef, ListPageParams, LocalIdentifier, MaterialiseContext, MediaTypeParams, MemberName, MenuDescriptor, NavigateOptions, Page, PageKeys, PageLinks, PageMeta, PageOf, PageParams, PageParamsOf, PaginatorDescriptor, PaginatorKind, PaginatorRole, PaginatorStrategy, PivotDescriptor, PivotInput, RawMediaKind, RawOutputMedia, RawResponseType, ReadQuery, ReadResult, ReadableRelationName, RelationDescriptor, RelationMutations, RelationName, RelationPageParams, RelationPaginator, RelationReadQuery, RelationView, RelationshipAccessor, RelationshipDocument, ResolvedPaginator, ResolvedValidator, ResourceAccessors, ResourceDescriptor, ResourceEnvelope, ResourceHandle, ResourceIdentifier, ResourceLinks, ResourceObjectView, SchemaMap, SingleReadQuery, SortQuery, SortToken, StrategyDescriptor, StructuralGuardError, ToDocumentOptions, TransportRawBody, TransportRequest, TransportResponse, TypeAccessor, TypeName, TypedPage, TypedReadQuery, UpdateInput, ValidationConfig, ValidationOption, Validator, WireResource, WriteDocument, WriteOptions, assertJsonApiDocument, createClient, execute, executeRaw, fetchTransport, hasErrorCode, materialise, mediaType, relatedPaginator, relatedPaginatorKind, remapPointer, remapRelationshipPointer, resolvePaginator, resolveValidator, runAtomic, serializeQuery, toDocument, toRelationshipDocument, validateDocument, withRemappedAtomicPaths, withRemappedPaths, withRemappedRelationshipPaths };
220
220
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -569,7 +569,7 @@ const append = (parts, key, value) => {
569
569
  if (value === void 0 || value === null || value === "") return;
570
570
  parts.push(`${key}=${encodeURIComponent(String(value))}`);
571
571
  };
572
- const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
572
+ const isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
573
573
  /**
574
574
  * Append a filter value under its (already-bracketed) key. A plain object is a STRUCTURED filter
575
575
  * value — a `Range`/`DateRange` deepObject (`{ min, max }`) — and recurses into nested bracketed
@@ -578,7 +578,7 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
578
578
  * wire as `filter[k]=[object Object]` and be silently ignored by the server (D23).
579
579
  */
580
580
  const appendFilterValue = (parts, key, value) => {
581
- if (isPlainObject(value)) {
581
+ if (isPlainObject$1(value)) {
582
582
  for (const [subKey, subValue] of Object.entries(value)) appendFilterValue(parts, `${key}[${subKey}]`, subValue);
583
583
  return;
584
584
  }
@@ -608,6 +608,22 @@ function serializeQuery(query) {
608
608
  * parsed error objects (tolerating a non-JSON error body with an empty list).
609
609
  */
610
610
  async function execute(ctx, req) {
611
+ return parseDocument((await send(ctx, req)).body);
612
+ }
613
+ async function executeRaw(ctx, req, as) {
614
+ const res = await send(ctx, req, as === "text" ? void 0 : as);
615
+ if (as === "text") return res.body === "" ? void 0 : res.body;
616
+ if (res.status === 204) return;
617
+ if (res.binary === void 0) throw new Error("The transport returned no binary body for a request that asked for one; a custom transport must read the response as binary when the request sets responseType.");
618
+ return res.binary;
619
+ }
620
+ /**
621
+ * Build the URL (a server link resolved against `baseUrl` and held to its origin, an absolute
622
+ * path verbatim, else `baseUrl + path`), apply content negotiation and any async per-request
623
+ * headers, then drive the transport. A non-2xx throws a {@link JsonApiError} carrying the parsed
624
+ * error objects (tolerating a non-JSON error body with an empty list).
625
+ */
626
+ async function send(ctx, req, responseType) {
611
627
  let url;
612
628
  let sendHeaders = true;
613
629
  if (req.link) {
@@ -631,15 +647,23 @@ async function execute(ctx, req) {
631
647
  headers
632
648
  };
633
649
  if (req.signal !== void 0) transportReq.signal = req.signal;
634
- if (req.body !== void 0) {
635
- headers["Content-Type"] = req.contentType ?? negotiated;
636
- transportReq.body = req.raw && typeof req.body === "string" ? req.body : JSON.stringify(req.body);
650
+ if (responseType !== void 0) transportReq.responseType = responseType;
651
+ if (req.body !== void 0 || req.method.toUpperCase() === "POST") {
652
+ const contentType = req.contentType === void 0 ? negotiated : req.contentType;
653
+ if (contentType !== null) headers["Content-Type"] = contentType;
637
654
  }
655
+ if (typeof req.body === "string" && req.raw) transportReq.body = req.body;
656
+ else if (req.raw && isRawBody(req.body)) transportReq.rawBody = req.body;
657
+ else if (req.body !== void 0) transportReq.body = JSON.stringify(req.body);
638
658
  if (ctx.headers && sendHeaders) mergeHeaders(headers, await ctx.headers());
639
659
  const res = await ctx.transport(transportReq);
640
- if (res.status >= 200 && res.status < 300) return parseDocument(res.body);
660
+ if (res.status >= 200 && res.status < 300) return res;
641
661
  throw new JsonApiError(res.status, parseErrors(res.body));
642
662
  }
663
+ /** Whether a raw body goes to the transport as is rather than through `JSON.stringify`. */
664
+ function isRawBody(body) {
665
+ return typeof Blob !== "undefined" && body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || typeof FormData !== "undefined" && body instanceof FormData || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams;
666
+ }
643
667
  /**
644
668
  * Merge a `HeadersInit` (object / tuple array / `Headers`) into the outgoing map,
645
669
  * preserving the caller's casing (unlike the WHATWG `Headers`, which lowercases). The
@@ -1027,13 +1051,23 @@ const fetchTransport = async (req) => {
1027
1051
  method: req.method,
1028
1052
  headers: req.headers
1029
1053
  };
1030
- if (req.body !== void 0) init.body = req.body;
1054
+ if (req.rawBody !== void 0) init.body = req.rawBody;
1055
+ else if (req.body !== void 0) init.body = req.body;
1031
1056
  if (req.signal !== void 0) init.signal = req.signal;
1032
1057
  const res = await fetch(req.url, init);
1033
1058
  const headers = {};
1034
1059
  res.headers.forEach((value, key) => {
1035
1060
  headers[key] = value;
1036
1061
  });
1062
+ if (req.responseType !== void 0 && res.ok) {
1063
+ const binary = req.responseType === "blob" ? await res.blob() : await res.arrayBuffer();
1064
+ return {
1065
+ status: res.status,
1066
+ headers,
1067
+ body: "",
1068
+ binary
1069
+ };
1070
+ }
1037
1071
  return {
1038
1072
  status: res.status,
1039
1073
  headers,
@@ -1144,36 +1178,145 @@ async function runWrite(ctx, spec) {
1144
1178
  * - `raw` — the caller's payload, sent with the action's declared media type (e.g.
1145
1179
  * `application/octet-stream`), falling back to a wildcard when the spec declared none.
1146
1180
  *
1181
+ * An action listing several request media types (`inputMedia`) sends the one the caller picks
1182
+ * with `options.contentType`, or JSON:API when listed, else the first.
1183
+ *
1147
1184
  * The response is shaped by the action's `output`: `document` materialises into the resource /
1148
1185
  * collection view, `meta` returns the document's top-level `meta`, `none` resolves `undefined` (a
1149
- * `204` / no body).
1186
+ * `204` / no body). An action declaring `rawOutput` answers in the media type the caller picks
1187
+ * with `options.accept`: JSON:API as above, or a `string`, a `Blob` or `ArrayBuffer`, or parsed
1188
+ * JSON by the media type's kind.
1150
1189
  */
1151
- async function runAction(ctx, action, vars, input) {
1190
+ async function runAction(ctx, action, vars, input, options = {}) {
1152
1191
  const req = {
1153
1192
  method: action.method ?? "POST",
1154
1193
  path: fill(action.path, vars)
1155
1194
  };
1195
+ if (options.signal !== void 0) req.signal = options.signal;
1156
1196
  let remap;
1157
- if (action.input === "document") if (action.inputType !== void 0) {
1197
+ const media = requestMedia(action, options.contentType);
1198
+ if (media !== void 0 && media.kind !== "document") {
1199
+ req.body = encodeFields(media.mediaType, media.kind, input);
1200
+ req.raw = true;
1201
+ req.contentType = sentContentType(media.mediaType, media.kind, req.body);
1202
+ } else if (action.input === "document") if (action.inputType !== void 0) {
1158
1203
  const inputType = action.inputType;
1159
1204
  req.body = toDocument(ctx.descriptor, inputType, input ?? {});
1160
1205
  remap = (error) => withRemappedPaths(error, ctx.descriptor, inputType);
1161
1206
  } else req.body = input;
1162
1207
  else if (action.input === "raw") {
1163
- req.body = input;
1164
- req.contentType = action.contentType ?? "*/*";
1208
+ req.body = encodeFields(action.contentType ?? "*/*", void 0, input);
1209
+ req.contentType = sentContentType(action.contentType ?? "*/*", void 0, req.body);
1165
1210
  req.raw = true;
1166
1211
  }
1167
- let doc;
1212
+ const raw = responseMedia(action, options.accept);
1168
1213
  try {
1169
- doc = await execute(ctx.request, req);
1214
+ if (raw !== void 0) {
1215
+ req.accept = raw.mediaType;
1216
+ if (raw.kind === "binary") return await executeRaw(ctx.request, req, options.responseType === "arrayBuffer" ? "arraybuffer" : "blob");
1217
+ const text = await executeRaw(ctx.request, req, "text");
1218
+ return raw.kind === "json" && text !== void 0 ? JSON.parse(text) : text;
1219
+ }
1220
+ const doc = await execute(ctx.request, req);
1221
+ if (action.output === "none" || doc === void 0) return;
1222
+ if (action.output === "meta") return doc.meta;
1223
+ return materialise(doc, ctx.materialise);
1170
1224
  } catch (error) {
1171
1225
  if (remap !== void 0 && error instanceof JsonApiError) throw remap(error);
1172
1226
  throw error;
1173
1227
  }
1174
- if (action.output === "none" || doc === void 0) return;
1175
- if (action.output === "meta") return doc.meta;
1176
- return materialise(doc, ctx.materialise);
1228
+ }
1229
+ /**
1230
+ * The request media type an action with several (`inputMedia`) sends: the caller's pick, matched
1231
+ * exactly or through a declared wildcard such as `image/*`, else JSON:API when listed, else the
1232
+ * first. `undefined` for an action with a single request media type.
1233
+ */
1234
+ function requestMedia(action, picked) {
1235
+ const listed = action.inputMedia;
1236
+ if (listed === void 0 || listed.length === 0) {
1237
+ if (picked !== void 0) throw new Error(`This action declares no request media types to choose "${picked}" from.`);
1238
+ return;
1239
+ }
1240
+ if (picked === void 0) {
1241
+ const media = listed.find((entry) => entry.mediaType === "application/vnd.api+json") ?? listed[0];
1242
+ return {
1243
+ mediaType: media.mediaType,
1244
+ kind: media.kind
1245
+ };
1246
+ }
1247
+ const essence = picked.split(";")[0].trim().toLowerCase();
1248
+ const match = listed.find((entry) => entry.mediaType === essence) ?? listed.find((entry) => entry.mediaType === `${essence.split("/")[0]}/*`) ?? listed.find((entry) => entry.mediaType === "*/*");
1249
+ if (match === void 0) throw new Error(`This action does not accept "${picked}"; it accepts ${listed.map((entry) => entry.mediaType).join(", ")}.`);
1250
+ return {
1251
+ mediaType: picked,
1252
+ kind: match.kind
1253
+ };
1254
+ }
1255
+ /**
1256
+ * A multipart or URL-encoded body given as a plain object of fields, encoded into a `FormData` or
1257
+ * `URLSearchParams`. Any other body, a ready `FormData` included, is sent as it is.
1258
+ */
1259
+ function encodeFields(mediaType, kind, body) {
1260
+ const multipart = kind === "multipart" || mediaType.startsWith("multipart/");
1261
+ if (!multipart && !(kind === "form" || mediaType === "application/x-www-form-urlencoded") || !isPlainObject(body)) return body;
1262
+ return multipart ? toFormData(body) : toSearchParams(body);
1263
+ }
1264
+ /** Whether a body is a plain object of fields rather than a ready payload. */
1265
+ function isPlainObject(value) {
1266
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1267
+ const proto = Object.getPrototypeOf(value);
1268
+ return proto === Object.prototype || proto === null;
1269
+ }
1270
+ /**
1271
+ * The entries a field contributes: nothing for `null` or `undefined`, one `name[]` entry per item
1272
+ * for an array (the bracket suffix is how PHP servers read a repeated field as a list), else one
1273
+ * `name` entry.
1274
+ */
1275
+ function fieldEntries(name, value) {
1276
+ if (value === null || value === void 0) return [];
1277
+ if (Array.isArray(value)) return value.filter((item) => item !== null && item !== void 0).map((item) => [`${name}[]`, item]);
1278
+ return [[name, value]];
1279
+ }
1280
+ /** A field's text value: booleans as `true` or `false`, everything else through `String`. */
1281
+ function fieldText(value) {
1282
+ return typeof value === "boolean" ? value ? "true" : "false" : String(value);
1283
+ }
1284
+ /** A multipart body from fields. A `File` keeps its name; a bare `Blob` is named after its field. */
1285
+ function toFormData(fields) {
1286
+ const data = new FormData();
1287
+ for (const [field, value] of Object.entries(fields)) for (const [name, item] of fieldEntries(field, value)) if (typeof Blob !== "undefined" && item instanceof Blob) {
1288
+ const fileName = typeof File !== "undefined" && item instanceof File && item.name !== "" ? item.name : field;
1289
+ data.append(name, item, fileName);
1290
+ } else data.append(name, fieldText(item));
1291
+ return data;
1292
+ }
1293
+ /** A URL-encoded body from fields. */
1294
+ function toSearchParams(fields) {
1295
+ const params = new URLSearchParams();
1296
+ for (const [field, value] of Object.entries(fields)) for (const [name, item] of fieldEntries(field, value)) params.append(name, fieldText(item));
1297
+ return params;
1298
+ }
1299
+ /**
1300
+ * The `Content-Type` for a non-JSON:API body. A multipart `FormData` body gets none, so the
1301
+ * transport writes the boundary. A wildcard media type takes a `Blob` body's own type.
1302
+ */
1303
+ function sentContentType(mediaType, kind, body) {
1304
+ if ((kind === "multipart" || mediaType.startsWith("multipart/")) && typeof FormData !== "undefined" && body instanceof FormData) return null;
1305
+ if (mediaType.includes("*") && typeof Blob !== "undefined" && body instanceof Blob) return body.type === "" ? mediaType : body.type;
1306
+ return mediaType;
1307
+ }
1308
+ /**
1309
+ * The non-JSON:API media type an action is asked to answer in: the caller's `accept` when it isn't
1310
+ * JSON:API, else, with no `accept`, the first raw media type of an action that answers in no
1311
+ * JSON:API document. `undefined` means a JSON:API response.
1312
+ */
1313
+ function responseMedia(action, accept) {
1314
+ const raw = action.rawOutput ?? [];
1315
+ if (accept === void 0) return action.output === "none" ? raw[0] : void 0;
1316
+ if (accept === "application/vnd.api+json") return;
1317
+ const media = raw.find((entry) => entry.mediaType === accept);
1318
+ if (media === void 0) throw new Error(`This action does not answer in "${accept}"; it answers in ${[...action.output === "none" ? [] : [JSON_API_MEDIA_TYPE], ...raw.map((entry) => entry.mediaType)].join(", ")}.`);
1319
+ return media;
1177
1320
  }
1178
1321
  /**
1179
1322
  * Build the typed `.actions` accessor at one scope: a plain object exposing exactly the
@@ -1185,7 +1328,7 @@ async function runAction(ctx, action, vars, input) {
1185
1328
  function actionsAccessor(ctx, type, scope, vars) {
1186
1329
  const actions = ctx.descriptor[type]?.actions ?? {};
1187
1330
  const out = {};
1188
- for (const [name, action] of Object.entries(actions)) if (action.scope === scope) out[name] = (input) => runAction(ctx, action, vars, input);
1331
+ for (const [name, action] of Object.entries(actions)) if (action.scope === scope) out[name] = (...args) => action.input === "none" ? runAction(ctx, action, vars, void 0, args[0]) : runAction(ctx, action, vars, args[0], args[1]);
1189
1332
  return out;
1190
1333
  }
1191
1334
  /**
@@ -1349,6 +1492,6 @@ function createClient(descriptor, options) {
1349
1492
  });
1350
1493
  }
1351
1494
  //#endregion
1352
- 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 };
1495
+ export { ATOMIC_EXT, ATOMIC_MEDIA_TYPE, CANONICAL_PAGE_KEYS, HANDLE_RESERVED, JSON_API_MEDIA_TYPE, JsonApiError, StructuralGuardError, assertJsonApiDocument, createClient, execute, executeRaw, fetchTransport, hasErrorCode, materialise, mediaType, relatedPaginator, relatedPaginatorKind, remapPointer, remapRelationshipPointer, resolvePaginator, resolveValidator, runAtomic, serializeQuery, toDocument, toRelationshipDocument, validateDocument, withRemappedAtomicPaths, withRemappedPaths, withRemappedRelationshipPaths };
1353
1496
 
1354
1497
  //# sourceMappingURL=index.mjs.map