openapi-typesafe-fetch 1.0.0-rc.5 → 1.0.0-rc.7

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/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  This package is a fork of `openapi-fetch`. The entries above `0.17.0` are releases of openapi-typesafe-fetch; `0.17.0` and the entries below it are the upstream history up to the fork point (upstream commit `d46a319a`), with `openapi-fetch` versions.
4
4
 
5
+ ## 1.0.0-rc.7
6
+
7
+ ### Patch Changes
8
+
9
+ - [#96](https://github.com/openapi-typesafe/openapi-typesafe/pull/96) [`171fa71`](https://github.com/openapi-typesafe/openapi-typesafe/commit/171fa71311107e4e26a9ae7a62b431662fb13de5) Thanks [@fedorello](https://github.com/fedorello)! - `SuccessResponseJSON`, `ErrorResponseJSON` and the `data` and `error` of a client made with
10
+ ``createClient<paths, `${string}/json`>()`` leave out a response that has no JSON media type. It
11
+ added its whole `content` map, so a `text/html` 500 typed the error as `{ "text/html": string }`,
12
+ a value the client never returns.
13
+
14
+ - [#96](https://github.com/openapi-typesafe/openapi-typesafe/pull/96) [`171fa71`](https://github.com/openapi-typesafe/openapi-typesafe/commit/171fa71311107e4e26a9ae7a62b431662fb13de5) Thanks [@fedorello](https://github.com/fedorello)! - A nested array or object in a path parameter (`{ id: [[1]] }`) now fails with an error that names
15
+ `pathSerializer()`, the option that sends such values. It told you to write a `querySerializer()`,
16
+ which does not apply to the path.
17
+
18
+ - [#96](https://github.com/openapi-typesafe/openapi-typesafe/pull/96) [`171fa71`](https://github.com/openapi-typesafe/openapi-typesafe/commit/171fa71311107e4e26a9ae7a62b431662fb13de5) Thanks [@fedorello](https://github.com/fedorello)! - A request's `querySerializer` option set to `undefined` (`{ array: { explode: undefined } }`, as a
19
+ wrapper passes its own optional option on) now takes the client's value, as an option left out
20
+ does. It replaced the client's value, so the default style was sent.
21
+
22
+ ## 1.0.0-rc.6
23
+
24
+ ### Patch Changes
25
+
26
+ - [#87](https://github.com/openapi-typesafe/openapi-typesafe/pull/87) [`dfb0fe3`](https://github.com/openapi-typesafe/openapi-typesafe/commit/dfb0fe3c61a75d2b8ca512577f4b699049171a64) Thanks [@fedorello](https://github.com/fedorello)! - Leave out `null` and `undefined` items and members of arrays and objects in every parameter style, also in the path and headers (`ids=1,null`, `filter=owner,undefined` and `x-p: a,,b,1` were sent before), and default `explode` to `true` for the `form` query style only, as OpenAPI does, so `{ array: { style: "pipeDelimited" } }` sends `id=3|4|5`. A nested value in a header parameter now fails with an error that names header parameters, not `querySerializer()`.
27
+
5
28
  ## 1.0.0-rc.5
6
29
 
7
30
  ### Minor Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # openapi-typesafe-fetch
2
2
 
3
- A type-safe `fetch` client for types generated by [`openapi-typesafe`](../openapi-typesafe). About 10.7 kB minified
3
+ A type-safe `fetch` client for types generated by [`openapi-typesafe`](../openapi-typesafe). About 11 kB minified
4
4
  (4 kB gzipped), no runtime dependencies, works in browsers, Node.js, Bun and Deno, and with React, Vue, Svelte or plain JavaScript.
5
5
 
6
6
  A maintained, drop-in successor to `openapi-fetch` (see the [repository README](../../README.md)).
@@ -26,7 +26,7 @@ const client = createClient<paths>({ baseUrl: "https://api.example.com/v1/" });
26
26
 
27
27
  const {
28
28
  data, // present on a 2xx response
29
- error, // present on a 4xx or 5xx response
29
+ error, // present on a response that is not 2xx
30
30
  } = await client.GET("/posts/{post_id}", {
31
31
  params: { path: { post_id: "123" } },
32
32
  });
@@ -206,8 +206,14 @@ type SuccessStatus<T> = [SuccessStatusKey<T>] extends [never] ? Extract<keyof T,
206
206
  * share its statuses, and a `default`-only path typed the data `undefined` (upstream).
207
207
  */
208
208
  type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, SuccessStatus<T>> : never;
209
+ /**
210
+ * The bodies of the responses `ResponseCode` in the media types `Media`. A response with a
211
+ * `content` map but no media type in `Media` adds nothing: it gave its whole map, so the error of
212
+ * `ErrorResponseJSON` for a `text/html` 500 was `{ "text/html": string }`, a body no response has.
213
+ * A response without `content` keeps its `content` type (`undefined` for `content?: never`).
214
+ */
209
215
  type GetResponseContent<T extends Record<string | number, any>, Media extends MediaType = MediaType, ResponseCode extends keyof T = keyof T> = ResponseCode extends keyof T ? {
210
- [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? FilterKeys<T[K]["content"], Media> extends never ? T[K]["content"] : FilterKeys<T[K]["content"], Media> : K extends keyof T ? T[K]["content"] : never;
216
+ [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? FilterKeys<T[K]["content"], Media> : K extends keyof T ? T[K]["content"] : never;
211
217
  }[ResponseCode] : never;
212
218
  /**
213
219
  * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
@@ -216,7 +222,7 @@ type GetResponseContent<T extends Record<string | number, any>, Media extends Me
216
222
  type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatusKey<T>> : never;
217
223
  /** Return first JSON-like 2XX response from a path + HTTP method */
218
224
  type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
219
- /** Return first JSON-like 5XX or 4XX response from a path + HTTP method */
225
+ /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
220
226
  type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
221
227
  /** Return JSON-like request body from a path + HTTP method */
222
228
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
@@ -206,8 +206,14 @@ type SuccessStatus<T> = [SuccessStatusKey<T>] extends [never] ? Extract<keyof T,
206
206
  * share its statuses, and a `default`-only path typed the data `undefined` (upstream).
207
207
  */
208
208
  type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, SuccessStatus<T>> : never;
209
+ /**
210
+ * The bodies of the responses `ResponseCode` in the media types `Media`. A response with a
211
+ * `content` map but no media type in `Media` adds nothing: it gave its whole map, so the error of
212
+ * `ErrorResponseJSON` for a `text/html` 500 was `{ "text/html": string }`, a body no response has.
213
+ * A response without `content` keeps its `content` type (`undefined` for `content?: never`).
214
+ */
209
215
  type GetResponseContent<T extends Record<string | number, any>, Media extends MediaType = MediaType, ResponseCode extends keyof T = keyof T> = ResponseCode extends keyof T ? {
210
- [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? FilterKeys<T[K]["content"], Media> extends never ? T[K]["content"] : FilterKeys<T[K]["content"], Media> : K extends keyof T ? T[K]["content"] : never;
216
+ [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? FilterKeys<T[K]["content"], Media> : K extends keyof T ? T[K]["content"] : never;
211
217
  }[ResponseCode] : never;
212
218
  /**
213
219
  * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
@@ -216,7 +222,7 @@ type GetResponseContent<T extends Record<string | number, any>, Media extends Me
216
222
  type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatusKey<T>> : never;
217
223
  /** Return first JSON-like 2XX response from a path + HTTP method */
218
224
  type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
219
- /** Return first JSON-like 5XX or 4XX response from a path + HTTP method */
225
+ /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
220
226
  type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
221
227
  /** Return JSON-like request body from a path + HTTP method */
222
228
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
package/dist/helpers.d.ts CHANGED
@@ -206,8 +206,14 @@ type SuccessStatus<T> = [SuccessStatusKey<T>] extends [never] ? Extract<keyof T,
206
206
  * share its statuses, and a `default`-only path typed the data `undefined` (upstream).
207
207
  */
208
208
  type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, SuccessStatus<T>> : never;
209
+ /**
210
+ * The bodies of the responses `ResponseCode` in the media types `Media`. A response with a
211
+ * `content` map but no media type in `Media` adds nothing: it gave its whole map, so the error of
212
+ * `ErrorResponseJSON` for a `text/html` 500 was `{ "text/html": string }`, a body no response has.
213
+ * A response without `content` keeps its `content` type (`undefined` for `content?: never`).
214
+ */
209
215
  type GetResponseContent<T extends Record<string | number, any>, Media extends MediaType = MediaType, ResponseCode extends keyof T = keyof T> = ResponseCode extends keyof T ? {
210
- [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? FilterKeys<T[K]["content"], Media> extends never ? T[K]["content"] : FilterKeys<T[K]["content"], Media> : K extends keyof T ? T[K]["content"] : never;
216
+ [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? FilterKeys<T[K]["content"], Media> : K extends keyof T ? T[K]["content"] : never;
211
217
  }[ResponseCode] : never;
212
218
  /**
213
219
  * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
@@ -216,7 +222,7 @@ type GetResponseContent<T extends Record<string | number, any>, Media extends Me
216
222
  type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatusKey<T>> : never;
217
223
  /** Return first JSON-like 2XX response from a path + HTTP method */
218
224
  type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
219
- /** Return first JSON-like 5XX or 4XX response from a path + HTTP method */
225
+ /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
220
226
  type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
221
227
  /** Return JSON-like request body from a path + HTTP method */
222
228
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
package/dist/index.cjs CHANGED
@@ -415,6 +415,7 @@ function headerParamValue(value) {
415
415
  if (value === null || typeof value !== "object" || isDate(value)) {
416
416
  return valueText(value);
417
417
  }
418
+ checkFlatMembers(value, HEADER_NESTED_VALUE_ERROR);
418
419
  const serialize = Array.isArray(value) ? serializeArrayParam : serializeObjectParam;
419
420
  return serialize("", value, HEADER_PARAM_STYLE) || void 0;
420
421
  }
@@ -425,11 +426,23 @@ function serializeHeaderParams(header) {
425
426
  const entries = Object.entries(header).map(([name, value]) => [name, headerParamValue(value)]);
426
427
  return Object.fromEntries(entries);
427
428
  }
429
+ const NESTED_VALUE_ERROR = "Deeply-nested arrays/objects aren\u2019t supported";
430
+ const HEADER_NESTED_VALUE_ERROR = `${NESTED_VALUE_ERROR} in header parameters.`;
431
+ const PATH_HOOK_HINT = "Provide your own `pathSerializer()` to handle these.";
432
+ const PATH_NESTED_VALUE_ERROR = `${NESTED_VALUE_ERROR} in path parameters. ${PATH_HOOK_HINT}`;
433
+ const QUERY_HOOK_HINT = "Provide your own `querySerializer()` to handle these.";
434
+ const QUERY_NESTED_VALUE_ERROR = `${NESTED_VALUE_ERROR}. ${QUERY_HOOK_HINT}`;
435
+ function isNestedValue(value) {
436
+ return Boolean(value) && typeof value === "object" && !isDate(value);
437
+ }
438
+ function checkFlatMembers(value, message) {
439
+ if (Object.values(value).some((member) => isNestedValue(member))) {
440
+ throw new Error(message);
441
+ }
442
+ }
428
443
  function encodeValue(value, allowReserved) {
429
- if (value && typeof value === "object" && !isDate(value)) {
430
- throw new Error(
431
- "Deeply-nested arrays/objects aren\u2019t supported. Provide your own `querySerializer()` to handle these."
432
- );
444
+ if (isNestedValue(value)) {
445
+ throw new Error(QUERY_NESTED_VALUE_ERROR);
433
446
  }
434
447
  const text = valueText(value);
435
448
  return allowReserved === true ? text : encodeURIComponent(text);
@@ -448,10 +461,15 @@ function joinExploded(values, style) {
448
461
  function serializeUnexplodedObject(name, value, options) {
449
462
  const values = [];
450
463
  for (const k in value) {
451
- values.push(
452
- encodeValue(k, options.allowReserved),
453
- encodeValue(value[k], options.allowReserved)
454
- );
464
+ if (value[k] != null) {
465
+ values.push(
466
+ encodeValue(k, options.allowReserved),
467
+ encodeValue(value[k], options.allowReserved)
468
+ );
469
+ }
470
+ }
471
+ if (!values.length) {
472
+ return "";
455
473
  }
456
474
  const final = values.join(",");
457
475
  switch (options.style) {
@@ -488,7 +506,11 @@ function serializeObjectParam(name, value, options) {
488
506
  }
489
507
  function serializeUnexplodedArray(name, value, options) {
490
508
  const joiner = { form: ",", spaceDelimited: "%20", pipeDelimited: "|" }[options.style] || ",";
491
- const final = value.map((v) => encodeValue(v, options.allowReserved)).join(joiner);
509
+ const items = value.filter((v) => v != null);
510
+ if (!items.length) {
511
+ return "";
512
+ }
513
+ const final = items.map((v) => encodeValue(v, options.allowReserved)).join(joiner);
492
514
  switch (options.style) {
493
515
  case "simple": {
494
516
  return final;
@@ -513,29 +535,29 @@ function serializeArrayParam(name, value, options) {
513
535
  }
514
536
  const values = [];
515
537
  for (const v of value) {
538
+ if (v == null) {
539
+ continue;
540
+ }
516
541
  if (options.style === "simple" || options.style === "label") {
517
542
  values.push(encodeValue(v, options.allowReserved));
518
- } else if (v != null) {
543
+ } else {
519
544
  values.push(serializePrimitiveParam(name, v, options));
520
545
  }
521
546
  }
522
547
  return values.length ? joinExploded(values, options.style) : "";
523
548
  }
549
+ function queryStyle(options, defaultStyle, allowReserved) {
550
+ const style = options?.style ?? defaultStyle;
551
+ return { style, explode: options?.explode ?? style === "form", allowReserved };
552
+ }
524
553
  function serializeQueryParam(name, value, options) {
525
554
  const allowReserved = options?.allowReserved || false;
526
555
  if (Array.isArray(value)) {
527
- return serializeArrayParam(name, value, {
528
- style: options?.array?.style ?? "form",
529
- explode: options?.array?.explode ?? true,
530
- allowReserved
531
- });
556
+ return serializeArrayParam(name, value, queryStyle(options?.array, "form", allowReserved));
532
557
  }
533
558
  if (value && typeof value === "object" && !isDate(value)) {
534
- return serializeObjectParam(name, value, {
535
- style: options?.object?.style ?? "deepObject",
536
- explode: options?.object?.explode ?? true,
537
- allowReserved
538
- });
559
+ const style = queryStyle(options?.object, "deepObject", allowReserved);
560
+ return serializeObjectParam(name, value, style);
539
561
  }
540
562
  return serializePrimitiveParam(name, value, options);
541
563
  }
@@ -557,11 +579,15 @@ function mergeQueryOptions(clientOptions, requestOptions) {
557
579
  const base = typeof clientOptions === "function" ? {} : clientOptions ?? {};
558
580
  return {
559
581
  ...base,
560
- ...requestOptions,
561
- array: { ...base.array, ...requestOptions.array },
562
- object: { ...base.object, ...requestOptions.object }
582
+ ...definedOptions(requestOptions),
583
+ array: { ...base.array, ...definedOptions(requestOptions.array) },
584
+ object: { ...base.object, ...definedOptions(requestOptions.object) }
563
585
  };
564
586
  }
587
+ function definedOptions(options) {
588
+ const entries = Object.entries(options ?? {}).filter(([, value]) => value !== void 0);
589
+ return Object.fromEntries(entries);
590
+ }
565
591
  function defaultPathSerializer(pathname, pathParams) {
566
592
  const matches = pathname.match(PATH_PARAM_RE);
567
593
  if (!matches) {
@@ -586,22 +612,26 @@ function defaultPathSerializer(pathname, pathParams) {
586
612
  if (!pathParams || pathParams[name] === void 0 || pathParams[name] === null) {
587
613
  continue;
588
614
  }
589
- const value = pathParams[name];
590
- let serialized;
591
- if (Array.isArray(value)) {
592
- serialized = serializeArrayParam(name, value, { style, explode });
593
- } else if (typeof value === "object" && !isDate(value)) {
594
- serialized = serializeObjectParam(name, value, { style, explode });
595
- } else if (style === "matrix") {
596
- serialized = `;${serializePrimitiveParam(name, value)}`;
597
- } else {
598
- serialized = style === "label" ? `.${encodeValue(value)}` : encodeValue(value);
599
- }
615
+ const serialized = serializePathParam(name, pathParams[name], { style, explode });
600
616
  nextURL = nextURL.replace(match, () => serialized);
601
617
  }
602
618
  checkSerializedPath(pathname, nextURL);
603
619
  return nextURL;
604
620
  }
621
+ function serializePathParam(name, value, options) {
622
+ if (Array.isArray(value)) {
623
+ checkFlatMembers(value, PATH_NESTED_VALUE_ERROR);
624
+ return serializeArrayParam(name, value, options);
625
+ }
626
+ if (typeof value === "object" && !isDate(value)) {
627
+ checkFlatMembers(value, PATH_NESTED_VALUE_ERROR);
628
+ return serializeObjectParam(name, value, options);
629
+ }
630
+ if (options.style === "matrix") {
631
+ return `;${serializePrimitiveParam(name, value)}`;
632
+ }
633
+ return options.style === "label" ? `.${encodeValue(value)}` : encodeValue(value);
634
+ }
605
635
  function checkSerializedPath(template, url) {
606
636
  const fixed = template.slice(0, template.search(PATH_PARAM_RE));
607
637
  const path = url.split(QUERY_OR_FRAGMENT_RE, 1)[0];