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

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,59 @@
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.5
6
+
7
+ ### Minor Changes
8
+
9
+ - [#68](https://github.com/openapi-typesafe/openapi-typesafe/pull/68) [`0f0792b`](https://github.com/openapi-typesafe/openapi-typesafe/commit/0f0792b8efa4f5139653cd285455f7d6165ddb95) Thanks [@fedorello](https://github.com/fedorello)! - A `FormData`, `Blob`, `File` or `ReadableStream` body from `undici` or another realm (an iframe) is
10
+ sent as it is: the client told a body by `instanceof`, so it JSON-encoded one of another class as
11
+ `{}`. `createClient` takes a `FormData` class next to `Request`: a multipart body is built with it,
12
+ and a FormData body of another class is copied into one of it, since undici's `Request` read the
13
+ platform's FormData as the text `[object FormData]`. With undici, pass
14
+ `createClient({ fetch, Request, FormData })` from the package. An instance of the platform's own
15
+ global class is still sent as it is, so the FormData, Blob and URLSearchParams of React Native and
16
+ the Blob and File of jsdom 20 (Jest 29), which have no type tag or no `arrayBuffer`, keep working.
17
+
18
+ - [#67](https://github.com/openapi-typesafe/openapi-typesafe/pull/67) [`fbaded2`](https://github.com/openapi-typesafe/openapi-typesafe/commit/fbaded241385d032d5b28d2c93d5e0a7d6ae292b) Thanks [@fedorello](https://github.com/fedorello)! - The client has a read-only `baseUrl`: the `baseUrl` option without its trailing slash, or `""`.
19
+
20
+ ### Patch Changes
21
+
22
+ - [#68](https://github.com/openapi-typesafe/openapi-typesafe/pull/68) [`0f0792b`](https://github.com/openapi-typesafe/openapi-typesafe/commit/0f0792b8efa4f5139653cd285455f7d6165ddb95) Thanks [@fedorello](https://github.com/fedorello)! - An empty success body read with `parseAs: "text"`, `"blob"`, `"arrayBuffer"` or `"stream"` gives
23
+ `""`, an empty `Blob` or `ArrayBuffer`, or `response.body`, as its type says. A 204, an answer to
24
+ HEAD or a `Content-Length: 0` gave `undefined`, while the same empty body without a length gave
25
+ `""`. With the default `parseAs: "json"` an empty body is still `undefined`.
26
+
27
+ - [#68](https://github.com/openapi-typesafe/openapi-typesafe/pull/68) [`0f0792b`](https://github.com/openapi-typesafe/openapi-typesafe/commit/0f0792b8efa4f5139653cd285455f7d6165ddb95) Thanks [@fedorello](https://github.com/fedorello)! - `params.header` values are written in the `simple` style, the only style OpenAPI has for headers:
28
+ a `Date` as ISO 8601 (it was local time), an array as `a,b` (it was `a, b`) and an object as
29
+ `role,admin` (it was `[object Object]`), not percent-encoded, also in the names. A nested object or
30
+ array inside one throws, as in the query. An empty array or object sends no header, and `null`
31
+ still removes one.
32
+
33
+ - [#68](https://github.com/openapi-typesafe/openapi-typesafe/pull/68) [`0f0792b`](https://github.com/openapi-typesafe/openapi-typesafe/commit/0f0792b8efa4f5139653cd285455f7d6165ddb95) Thanks [@fedorello](https://github.com/fedorello)! - A form or multipart `Content-Type` with spaces or tabs before its parameters
34
+ (`application/x-www-form-urlencoded ; charset=utf-8`, RFC 9110 OWS) gives form data or `FormData`
35
+ again, in the runtime and in the result type of `defaultBodySerializer`; the object was sent as
36
+ JSON. The JSON check already took them.
37
+
38
+ - [#68](https://github.com/openapi-typesafe/openapi-typesafe/pull/68) [`0f0792b`](https://github.com/openapi-typesafe/openapi-typesafe/commit/0f0792b8efa4f5139653cd285455f7d6165ddb95) Thanks [@fedorello](https://github.com/fedorello)! - `style` and `explode` of the `array` and `object` options of `QuerySerializerOptions` are optional,
39
+ and one left out takes the client's value, else its default (`form` or `deepObject`, exploded): a
40
+ request's `{ array: { explode: false } }` keeps the client's `style`. `onError` may return an
41
+ `Error` of another realm (an iframe, `node:vm`); it was rejected as not an error.
42
+
43
+ - [#68](https://github.com/openapi-typesafe/openapi-typesafe/pull/68) [`0f0792b`](https://github.com/openapi-typesafe/openapi-typesafe/commit/0f0792b8efa4f5139653cd285455f7d6165ddb95) Thanks [@fedorello](https://github.com/fedorello)! - The query of a request whose path template has a fragment (`/docs#intro`) goes before the
44
+ fragment, joined with a query the template already has. It was added after the `#`, where it is
45
+ part of the fragment, so `fetch` never sent it.
46
+
47
+ - [#75](https://github.com/openapi-typesafe/openapi-typesafe/pull/75) [`a74608e`](https://github.com/openapi-typesafe/openapi-typesafe/commit/a74608ec36b0a938594e0d0fac10c9fb222c1742) Thanks [@fedorello](https://github.com/fedorello)! - `Readable` and `Writable` find the `$Read` and `$Write` properties of an object with an index
48
+ signature (`additionalProperties`). The keys were read through the index signature, so no
49
+ property was marked: a request body required the `readOnly` `id`, and a response kept the
50
+ `writeOnly` password. The helper types that `--read-write-markers` writes are fixed the same way.
51
+
52
+ - [#68](https://github.com/openapi-typesafe/openapi-typesafe/pull/68) [`0f0792b`](https://github.com/openapi-typesafe/openapi-typesafe/commit/0f0792b8efa4f5139653cd285455f7d6165ddb95) Thanks [@fedorello](https://github.com/fedorello)! - A response is typed as data or an error by the first digit of its status, as `response.ok` reads
53
+ it: a declared 205, 208 or 226 types `data` with its body, and a 419, a 3XX (read with
54
+ `redirect: "manual"`) or a 1XX types `error` with its body.
55
+ The types classified by two closed lists of statuses, `OkStatus` and `ErrorStatus`, which stay
56
+ exported. New helper types `SuccessStatusKey` and `ErrorStatusKey` name the statuses of each kind.
57
+
5
58
  ## 1.0.0-rc.4
6
59
 
7
60
  ### Minor Changes
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # openapi-typesafe-fetch
2
2
 
3
- A type-safe `fetch` client for types generated by [`openapi-typesafe`](../openapi-typesafe). About 9 kB minified
4
- (3.7 kB gzipped), no runtime dependencies, works in browsers, Node.js, Bun and Deno, and with React, Vue, Svelte or plain JavaScript.
3
+ A type-safe `fetch` client for types generated by [`openapi-typesafe`](../openapi-typesafe). About 10.7 kB minified
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)).
7
7
 
@@ -2,12 +2,32 @@
2
2
  type HttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace"
3
3
  /** OpenAPI 3.2: a safe, idempotent request with a body. */
4
4
  | "query";
5
- /** 2XX statuses */
5
+ /**
6
+ * Some 2XX statuses, kept for code that names them. The client does not classify by this list: a
7
+ * status is a success or an error by its first digit (`SuccessStatusKey`, `ErrorStatusKey`).
8
+ */
6
9
  type OkStatus = 200 | 201 | 202 | 203 | 204 | 206 | 207 | "2XX";
7
- /** 4XX and 5XX statuses */
10
+ /** Some 4XX and 5XX statuses and `default`, kept like `OkStatus`. */
8
11
  type ErrorStatus = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | "5XX" | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 444 | 450 | 451 | 497 | 498 | 499 | "4XX" | "default";
12
+ type StatusDigit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
13
+ /**
14
+ * The keys of a Response Object Map whose text matches a pattern. A generated map has numeric keys
15
+ * (`200`) and string ones (`"2XX"`, `"default"`), and a numeric literal matches no template
16
+ * literal type, so each key is matched by its text.
17
+ */
18
+ type StatusKeysMatching<Keys, Pattern extends string> = Keys extends string | number ? `${Keys}` extends Pattern ? Keys : never : never;
19
+ /**
20
+ * The success statuses of a Response Object Map: every 2XX one. A closed list of statuses left out
21
+ * the ones it did not name (205, 208, 226), and their bodies were typed as no data.
22
+ */
23
+ type SuccessStatusKey<T> = StatusKeysMatching<keyof T, `2${StatusDigit}${StatusDigit}` | "2XX">;
24
+ /**
25
+ * The error statuses of a Response Object Map: every 1XX, 3XX, 4XX and 5XX one, and `default`. The
26
+ * client returns each response that is not `ok` as `error`, also a 3XX with `redirect: "manual"`.
27
+ */
28
+ type ErrorStatusKey<T> = StatusKeysMatching<keyof T, `${1 | 3 | 4 | 5}${StatusDigit}${StatusDigit}` | `${1 | 3 | 4 | 5}XX` | "default">;
9
29
  /** Get a union of OK Statuses */
10
- type OKStatusUnion<T> = FilterKeys<T, OkStatus>;
30
+ type OKStatusUnion<T> = FilterKeys<T, SuccessStatusKey<T>>;
11
31
  /** Get first error status, in order */
12
32
  type FirstErrorStatus<T> = T extends {
13
33
  500: unknown;
@@ -178,7 +198,7 @@ type OperationRequestBodyContent<T> = FilterKeys<OperationRequestBodyMediaConten
178
198
  * Responses Object), so then it covers every success (upstream #1862). With a 2XX status declared,
179
199
  * `default` is left to the errors, as before.
180
200
  */
181
- type SuccessStatus<T> = [Extract<keyof T, OkStatus>] extends [never] ? "default" : OkStatus;
201
+ type SuccessStatus<T> = [SuccessStatusKey<T>] extends [never] ? Extract<keyof T, "default"> : SuccessStatusKey<T>;
182
202
  /**
183
203
  * Return all 2XX responses from a Response Object Map, or `default` if it declares no 2XX one. For
184
204
  * a union of maps (a call with a union of paths), the union of each map's responses: `keyof` of a
@@ -190,10 +210,10 @@ type GetResponseContent<T extends Record<string | number, any>, Media extends Me
190
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;
191
211
  }[ResponseCode] : never;
192
212
  /**
193
- * Return all 5XX and 4XX responses (in that order) from a Response Object Map; for a union of maps,
194
- * the union of each map's responses, as `SuccessResponse`.
213
+ * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
214
+ * of maps, the union of each map's responses, as `SuccessResponse`.
195
215
  */
196
- type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatus> : never;
216
+ type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatusKey<T>> : never;
197
217
  /** Return first JSON-like 2XX response from a path + HTTP method */
198
218
  type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
199
219
  /** Return first JSON-like 5XX or 4XX response from a path + HTTP method */
@@ -258,10 +278,13 @@ type ReadWriteLeaf = ReadWritePrimitive | ((...args: never) => unknown);
258
278
  * `any` key is not marked either (`0 extends 1 & T[K]` is true only for `any`): a check on `any`
259
279
  * takes both branches, so `Readable` dropped the key and `Writable` typed it `undefined`. The
260
280
  * checks are not distributive (their check types are not type parameters), so `never` is seen.
281
+ * The keys are read by remapping, not as `{ ... }[keyof T]`: with an index signature (from
282
+ * `additionalProperties`) `keyof T` is `string`, which indexes the signature only, so no property
283
+ * was marked and `Writable` made a readOnly property a required `never`.
261
284
  */
262
- type ReadWriteMarkedKeys<T, Marker> = {
263
- [K in keyof T]-?: 0 extends 1 & T[K] ? never : NonNullable<T[K]> extends never ? never : NonNullable<T[K]> extends Marker ? K : never;
264
- }[keyof T];
285
+ type ReadWriteMarkedKeys<T, Marker> = keyof {
286
+ [K in keyof T as 0 extends 1 & T[K] ? never : NonNullable<T[K]> extends never ? never : NonNullable<T[K]> extends Marker ? K : never]: never;
287
+ };
265
288
  /**
266
289
  * Resolve an array or a tuple element by element. An array uses `T[number]`, not `infer E`: for an
267
290
  * intersection of arrays (`allOf`) inference keeps one member and narrowing breaks (upstream
@@ -296,4 +319,4 @@ type Writable<T> = T extends $Read<any> ? never : T extends $Write<infer U> ? Wr
296
319
  [K in ReadWriteMarkedKeys<T, $Read<any>>]?: never;
297
320
  } : T;
298
321
 
299
- export type { $Read, $Write, ErrorResponse, ErrorResponseJSON, ErrorStatus, FilterKeys, FindRequiredKeys, FirstErrorStatus, GetResponseContent, GetValueWithDefault, HasRequiredKeys, HttpMethod, IsOperationRequestBodyOptional, JSONLike, MediaType, OKStatusUnion, OkStatus, OperationObject, OperationRequestBody, OperationRequestBodyContent, OperationRequestBodyMediaContent, PathItemObject, PathsWithMethod, Readable, RequestBodyJSON, RequiredKeysOf, ResponseContent, ResponseObjectMap, SuccessResponse, SuccessResponseJSON, Writable };
322
+ export type { $Read, $Write, ErrorResponse, ErrorResponseJSON, ErrorStatus, ErrorStatusKey, FilterKeys, FindRequiredKeys, FirstErrorStatus, GetResponseContent, GetValueWithDefault, HasRequiredKeys, HttpMethod, IsOperationRequestBodyOptional, JSONLike, MediaType, OKStatusUnion, OkStatus, OperationObject, OperationRequestBody, OperationRequestBodyContent, OperationRequestBodyMediaContent, PathItemObject, PathsWithMethod, Readable, RequestBodyJSON, RequiredKeysOf, ResponseContent, ResponseObjectMap, SuccessResponse, SuccessResponseJSON, SuccessStatusKey, Writable };
@@ -2,12 +2,32 @@
2
2
  type HttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace"
3
3
  /** OpenAPI 3.2: a safe, idempotent request with a body. */
4
4
  | "query";
5
- /** 2XX statuses */
5
+ /**
6
+ * Some 2XX statuses, kept for code that names them. The client does not classify by this list: a
7
+ * status is a success or an error by its first digit (`SuccessStatusKey`, `ErrorStatusKey`).
8
+ */
6
9
  type OkStatus = 200 | 201 | 202 | 203 | 204 | 206 | 207 | "2XX";
7
- /** 4XX and 5XX statuses */
10
+ /** Some 4XX and 5XX statuses and `default`, kept like `OkStatus`. */
8
11
  type ErrorStatus = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | "5XX" | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 444 | 450 | 451 | 497 | 498 | 499 | "4XX" | "default";
12
+ type StatusDigit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
13
+ /**
14
+ * The keys of a Response Object Map whose text matches a pattern. A generated map has numeric keys
15
+ * (`200`) and string ones (`"2XX"`, `"default"`), and a numeric literal matches no template
16
+ * literal type, so each key is matched by its text.
17
+ */
18
+ type StatusKeysMatching<Keys, Pattern extends string> = Keys extends string | number ? `${Keys}` extends Pattern ? Keys : never : never;
19
+ /**
20
+ * The success statuses of a Response Object Map: every 2XX one. A closed list of statuses left out
21
+ * the ones it did not name (205, 208, 226), and their bodies were typed as no data.
22
+ */
23
+ type SuccessStatusKey<T> = StatusKeysMatching<keyof T, `2${StatusDigit}${StatusDigit}` | "2XX">;
24
+ /**
25
+ * The error statuses of a Response Object Map: every 1XX, 3XX, 4XX and 5XX one, and `default`. The
26
+ * client returns each response that is not `ok` as `error`, also a 3XX with `redirect: "manual"`.
27
+ */
28
+ type ErrorStatusKey<T> = StatusKeysMatching<keyof T, `${1 | 3 | 4 | 5}${StatusDigit}${StatusDigit}` | `${1 | 3 | 4 | 5}XX` | "default">;
9
29
  /** Get a union of OK Statuses */
10
- type OKStatusUnion<T> = FilterKeys<T, OkStatus>;
30
+ type OKStatusUnion<T> = FilterKeys<T, SuccessStatusKey<T>>;
11
31
  /** Get first error status, in order */
12
32
  type FirstErrorStatus<T> = T extends {
13
33
  500: unknown;
@@ -178,7 +198,7 @@ type OperationRequestBodyContent<T> = FilterKeys<OperationRequestBodyMediaConten
178
198
  * Responses Object), so then it covers every success (upstream #1862). With a 2XX status declared,
179
199
  * `default` is left to the errors, as before.
180
200
  */
181
- type SuccessStatus<T> = [Extract<keyof T, OkStatus>] extends [never] ? "default" : OkStatus;
201
+ type SuccessStatus<T> = [SuccessStatusKey<T>] extends [never] ? Extract<keyof T, "default"> : SuccessStatusKey<T>;
182
202
  /**
183
203
  * Return all 2XX responses from a Response Object Map, or `default` if it declares no 2XX one. For
184
204
  * a union of maps (a call with a union of paths), the union of each map's responses: `keyof` of a
@@ -190,10 +210,10 @@ type GetResponseContent<T extends Record<string | number, any>, Media extends Me
190
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;
191
211
  }[ResponseCode] : never;
192
212
  /**
193
- * Return all 5XX and 4XX responses (in that order) from a Response Object Map; for a union of maps,
194
- * the union of each map's responses, as `SuccessResponse`.
213
+ * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
214
+ * of maps, the union of each map's responses, as `SuccessResponse`.
195
215
  */
196
- type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatus> : never;
216
+ type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatusKey<T>> : never;
197
217
  /** Return first JSON-like 2XX response from a path + HTTP method */
198
218
  type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
199
219
  /** Return first JSON-like 5XX or 4XX response from a path + HTTP method */
@@ -258,10 +278,13 @@ type ReadWriteLeaf = ReadWritePrimitive | ((...args: never) => unknown);
258
278
  * `any` key is not marked either (`0 extends 1 & T[K]` is true only for `any`): a check on `any`
259
279
  * takes both branches, so `Readable` dropped the key and `Writable` typed it `undefined`. The
260
280
  * checks are not distributive (their check types are not type parameters), so `never` is seen.
281
+ * The keys are read by remapping, not as `{ ... }[keyof T]`: with an index signature (from
282
+ * `additionalProperties`) `keyof T` is `string`, which indexes the signature only, so no property
283
+ * was marked and `Writable` made a readOnly property a required `never`.
261
284
  */
262
- type ReadWriteMarkedKeys<T, Marker> = {
263
- [K in keyof T]-?: 0 extends 1 & T[K] ? never : NonNullable<T[K]> extends never ? never : NonNullable<T[K]> extends Marker ? K : never;
264
- }[keyof T];
285
+ type ReadWriteMarkedKeys<T, Marker> = keyof {
286
+ [K in keyof T as 0 extends 1 & T[K] ? never : NonNullable<T[K]> extends never ? never : NonNullable<T[K]> extends Marker ? K : never]: never;
287
+ };
265
288
  /**
266
289
  * Resolve an array or a tuple element by element. An array uses `T[number]`, not `infer E`: for an
267
290
  * intersection of arrays (`allOf`) inference keeps one member and narrowing breaks (upstream
@@ -296,4 +319,4 @@ type Writable<T> = T extends $Read<any> ? never : T extends $Write<infer U> ? Wr
296
319
  [K in ReadWriteMarkedKeys<T, $Read<any>>]?: never;
297
320
  } : T;
298
321
 
299
- export type { $Read, $Write, ErrorResponse, ErrorResponseJSON, ErrorStatus, FilterKeys, FindRequiredKeys, FirstErrorStatus, GetResponseContent, GetValueWithDefault, HasRequiredKeys, HttpMethod, IsOperationRequestBodyOptional, JSONLike, MediaType, OKStatusUnion, OkStatus, OperationObject, OperationRequestBody, OperationRequestBodyContent, OperationRequestBodyMediaContent, PathItemObject, PathsWithMethod, Readable, RequestBodyJSON, RequiredKeysOf, ResponseContent, ResponseObjectMap, SuccessResponse, SuccessResponseJSON, Writable };
322
+ export type { $Read, $Write, ErrorResponse, ErrorResponseJSON, ErrorStatus, ErrorStatusKey, FilterKeys, FindRequiredKeys, FirstErrorStatus, GetResponseContent, GetValueWithDefault, HasRequiredKeys, HttpMethod, IsOperationRequestBodyOptional, JSONLike, MediaType, OKStatusUnion, OkStatus, OperationObject, OperationRequestBody, OperationRequestBodyContent, OperationRequestBodyMediaContent, PathItemObject, PathsWithMethod, Readable, RequestBodyJSON, RequiredKeysOf, ResponseContent, ResponseObjectMap, SuccessResponse, SuccessResponseJSON, SuccessStatusKey, Writable };
package/dist/helpers.d.ts CHANGED
@@ -2,12 +2,32 @@
2
2
  type HttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace"
3
3
  /** OpenAPI 3.2: a safe, idempotent request with a body. */
4
4
  | "query";
5
- /** 2XX statuses */
5
+ /**
6
+ * Some 2XX statuses, kept for code that names them. The client does not classify by this list: a
7
+ * status is a success or an error by its first digit (`SuccessStatusKey`, `ErrorStatusKey`).
8
+ */
6
9
  type OkStatus = 200 | 201 | 202 | 203 | 204 | 206 | 207 | "2XX";
7
- /** 4XX and 5XX statuses */
10
+ /** Some 4XX and 5XX statuses and `default`, kept like `OkStatus`. */
8
11
  type ErrorStatus = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | "5XX" | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 444 | 450 | 451 | 497 | 498 | 499 | "4XX" | "default";
12
+ type StatusDigit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
13
+ /**
14
+ * The keys of a Response Object Map whose text matches a pattern. A generated map has numeric keys
15
+ * (`200`) and string ones (`"2XX"`, `"default"`), and a numeric literal matches no template
16
+ * literal type, so each key is matched by its text.
17
+ */
18
+ type StatusKeysMatching<Keys, Pattern extends string> = Keys extends string | number ? `${Keys}` extends Pattern ? Keys : never : never;
19
+ /**
20
+ * The success statuses of a Response Object Map: every 2XX one. A closed list of statuses left out
21
+ * the ones it did not name (205, 208, 226), and their bodies were typed as no data.
22
+ */
23
+ type SuccessStatusKey<T> = StatusKeysMatching<keyof T, `2${StatusDigit}${StatusDigit}` | "2XX">;
24
+ /**
25
+ * The error statuses of a Response Object Map: every 1XX, 3XX, 4XX and 5XX one, and `default`. The
26
+ * client returns each response that is not `ok` as `error`, also a 3XX with `redirect: "manual"`.
27
+ */
28
+ type ErrorStatusKey<T> = StatusKeysMatching<keyof T, `${1 | 3 | 4 | 5}${StatusDigit}${StatusDigit}` | `${1 | 3 | 4 | 5}XX` | "default">;
9
29
  /** Get a union of OK Statuses */
10
- type OKStatusUnion<T> = FilterKeys<T, OkStatus>;
30
+ type OKStatusUnion<T> = FilterKeys<T, SuccessStatusKey<T>>;
11
31
  /** Get first error status, in order */
12
32
  type FirstErrorStatus<T> = T extends {
13
33
  500: unknown;
@@ -178,7 +198,7 @@ type OperationRequestBodyContent<T> = FilterKeys<OperationRequestBodyMediaConten
178
198
  * Responses Object), so then it covers every success (upstream #1862). With a 2XX status declared,
179
199
  * `default` is left to the errors, as before.
180
200
  */
181
- type SuccessStatus<T> = [Extract<keyof T, OkStatus>] extends [never] ? "default" : OkStatus;
201
+ type SuccessStatus<T> = [SuccessStatusKey<T>] extends [never] ? Extract<keyof T, "default"> : SuccessStatusKey<T>;
182
202
  /**
183
203
  * Return all 2XX responses from a Response Object Map, or `default` if it declares no 2XX one. For
184
204
  * a union of maps (a call with a union of paths), the union of each map's responses: `keyof` of a
@@ -190,10 +210,10 @@ type GetResponseContent<T extends Record<string | number, any>, Media extends Me
190
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;
191
211
  }[ResponseCode] : never;
192
212
  /**
193
- * Return all 5XX and 4XX responses (in that order) from a Response Object Map; for a union of maps,
194
- * the union of each map's responses, as `SuccessResponse`.
213
+ * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
214
+ * of maps, the union of each map's responses, as `SuccessResponse`.
195
215
  */
196
- type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatus> : never;
216
+ type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatusKey<T>> : never;
197
217
  /** Return first JSON-like 2XX response from a path + HTTP method */
198
218
  type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
199
219
  /** Return first JSON-like 5XX or 4XX response from a path + HTTP method */
@@ -258,10 +278,13 @@ type ReadWriteLeaf = ReadWritePrimitive | ((...args: never) => unknown);
258
278
  * `any` key is not marked either (`0 extends 1 & T[K]` is true only for `any`): a check on `any`
259
279
  * takes both branches, so `Readable` dropped the key and `Writable` typed it `undefined`. The
260
280
  * checks are not distributive (their check types are not type parameters), so `never` is seen.
281
+ * The keys are read by remapping, not as `{ ... }[keyof T]`: with an index signature (from
282
+ * `additionalProperties`) `keyof T` is `string`, which indexes the signature only, so no property
283
+ * was marked and `Writable` made a readOnly property a required `never`.
261
284
  */
262
- type ReadWriteMarkedKeys<T, Marker> = {
263
- [K in keyof T]-?: 0 extends 1 & T[K] ? never : NonNullable<T[K]> extends never ? never : NonNullable<T[K]> extends Marker ? K : never;
264
- }[keyof T];
285
+ type ReadWriteMarkedKeys<T, Marker> = keyof {
286
+ [K in keyof T as 0 extends 1 & T[K] ? never : NonNullable<T[K]> extends never ? never : NonNullable<T[K]> extends Marker ? K : never]: never;
287
+ };
265
288
  /**
266
289
  * Resolve an array or a tuple element by element. An array uses `T[number]`, not `infer E`: for an
267
290
  * intersection of arrays (`allOf`) inference keeps one member and narrowing breaks (upstream
@@ -296,4 +319,4 @@ type Writable<T> = T extends $Read<any> ? never : T extends $Write<infer U> ? Wr
296
319
  [K in ReadWriteMarkedKeys<T, $Read<any>>]?: never;
297
320
  } : T;
298
321
 
299
- export type { $Read, $Write, ErrorResponse, ErrorResponseJSON, ErrorStatus, FilterKeys, FindRequiredKeys, FirstErrorStatus, GetResponseContent, GetValueWithDefault, HasRequiredKeys, HttpMethod, IsOperationRequestBodyOptional, JSONLike, MediaType, OKStatusUnion, OkStatus, OperationObject, OperationRequestBody, OperationRequestBodyContent, OperationRequestBodyMediaContent, PathItemObject, PathsWithMethod, Readable, RequestBodyJSON, RequiredKeysOf, ResponseContent, ResponseObjectMap, SuccessResponse, SuccessResponseJSON, Writable };
322
+ export type { $Read, $Write, ErrorResponse, ErrorResponseJSON, ErrorStatus, ErrorStatusKey, FilterKeys, FindRequiredKeys, FirstErrorStatus, GetResponseContent, GetValueWithDefault, HasRequiredKeys, HttpMethod, IsOperationRequestBodyOptional, JSONLike, MediaType, OKStatusUnion, OkStatus, OperationObject, OperationRequestBody, OperationRequestBodyContent, OperationRequestBodyMediaContent, PathItemObject, PathsWithMethod, Readable, RequestBodyJSON, RequiredKeysOf, ResponseContent, ResponseObjectMap, SuccessResponse, SuccessResponseJSON, SuccessStatusKey, Writable };
package/dist/index.cjs CHANGED
@@ -7,11 +7,18 @@ const DOT_SEGMENT_RE = /^\.\.?$/;
7
7
  const QUERY_OR_FRAGMENT_RE = /[?#]/;
8
8
  const JSON_MEDIA_TYPE_RE = /^[^/]+\/([^;]*\+)?json\s*(;|$)/i;
9
9
  const TEXT_MEDIA_TYPE_RE = /^text\//i;
10
- const FORM_MEDIA_TYPE_RE = /^application\/x-www-form-urlencoded(;|$)/i;
11
- const MULTIPART_MEDIA_TYPE_RE = /^multipart\/form-data(;|$)/i;
10
+ const FORM_MEDIA_TYPE_RE = /^application\/x-www-form-urlencoded[ \t]*(;|$)/i;
11
+ const MULTIPART_MEDIA_TYPE_RE = /^multipart\/form-data[ \t]*(;|$)/i;
12
12
  const NESTED_FORM_VALUE_MESSAGE = "Nested form values need your own `bodySerializer()`.";
13
13
  const BOUNDARY_PARAMETER_RE = /;\s*boundary=/i;
14
14
  const CLIENT_BUILT_FORM = Symbol.for("openapi-typesafe-fetch/form");
15
+ const TYPE_TAG_START = "[object ".length;
16
+ const TYPE_TAG_END = -1;
17
+ const BLOB_CLASSES = ["Blob", "File"];
18
+ const RAW_BODY_CLASSES = ["URLSearchParams", "ArrayBuffer"];
19
+ const FORM_DATA_METHODS = ["append", "entries"];
20
+ const BLOB_METHODS = ["arrayBuffer"];
21
+ const STREAM_METHODS = ["getReader"];
15
22
  const supportsRequestInitExt = () => {
16
23
  return typeof process === "object" && process.versions?.undici;
17
24
  };
@@ -24,6 +31,7 @@ function createClient(clientOptions) {
24
31
  let {
25
32
  baseUrl = "",
26
33
  Request: CustomRequest = globalThis.Request,
34
+ FormData: CustomFormData = globalThis.FormData,
27
35
  fetch: baseFetch = globalThis.fetch,
28
36
  querySerializer: globalQuerySerializer,
29
37
  bodySerializer: globalBodySerializer,
@@ -56,12 +64,10 @@ function createClient(clientOptions) {
56
64
  }
57
65
  let querySerializer = typeof globalQuerySerializer === "function" ? globalQuerySerializer : createQuerySerializer(globalQuerySerializer);
58
66
  if (requestQuerySerializer) {
59
- querySerializer = typeof requestQuerySerializer === "function" ? requestQuerySerializer : createQuerySerializer({
60
- ...typeof globalQuerySerializer === "object" ? globalQuerySerializer : {},
61
- ...requestQuerySerializer
62
- });
67
+ querySerializer = typeof requestQuerySerializer === "function" ? requestQuerySerializer : createQuerySerializer(mergeQueryOptions(globalQuerySerializer, requestQuerySerializer));
63
68
  }
64
69
  const pathSerializer = requestPathSerializer || globalPathSerializer || defaultPathSerializer;
70
+ const headerParams = serializeHeaderParams(params.header);
65
71
  const serializedBody = body === void 0 ? void 0 : bodySerializer(
66
72
  body,
67
73
  // Note: we declare mergeHeaders() both here and below because it’s a bit of a
@@ -70,29 +76,30 @@ function createClient(clientOptions) {
70
76
  // content-type header. So we re-merge below, setting the content-type at the very
71
77
  // beginning to be overwritten. Lastly, based on the way headers work, it’s not a simple
72
78
  // “present-or-not” check because null intentionally un-sets headers.
73
- mergeHeaders(baseHeaders, headers, params.header)
79
+ mergeHeaders(baseHeaders, headers, headerParams)
74
80
  );
81
+ const requestBody = isFormData(serializedBody) ? asFormDataOf(CustomFormData, serializedBody) : serializedBody;
75
82
  const finalHeaders = mergeHeaders(
76
83
  // with no body, we should not to set Content-Type
77
- serializedBody === void 0 || // a raw body (FormData, Blob, ...) gets its Content-Type from the platform, or none
78
- isRawBody(serializedBody) ? {} : {
84
+ requestBody === void 0 || // a raw body (FormData, Blob, ...) gets its Content-Type from the platform, or none
85
+ isRawBody(requestBody) ? {} : {
79
86
  "Content-Type": "application/json"
80
87
  },
81
88
  baseHeaders,
82
89
  headers,
83
- params.header
90
+ headerParams
84
91
  );
85
- if (isPlatformInstance(serializedBody, "FormData")) {
86
- dropFormContentType(finalHeaders, serializedBody[CLIENT_BUILT_FORM] === true);
92
+ if (isFormData(requestBody)) {
93
+ dropFormContentType(finalHeaders, requestBody[CLIENT_BUILT_FORM] === true);
87
94
  }
88
95
  const finalMiddlewares = [...globalMiddlewares, ...requestMiddlewares];
89
96
  const requestInit = {
90
97
  redirect: "follow",
91
98
  // The Fetch standard requires `duplex: "half"` (its only value) for a stream body.
92
- duplex: isStreamBody(serializedBody) ? "half" : void 0,
99
+ duplex: isStreamBody(requestBody) ? "half" : void 0,
93
100
  ...baseOptions,
94
101
  ...init,
95
- body: serializedBody,
102
+ body: requestBody,
96
103
  headers: finalHeaders
97
104
  };
98
105
  let id;
@@ -169,7 +176,7 @@ function createClient(clientOptions) {
169
176
  response = result;
170
177
  break;
171
178
  }
172
- if (result instanceof Error) {
179
+ if (isError(result)) {
173
180
  errorAfterMiddleware = result;
174
181
  continue;
175
182
  }
@@ -209,16 +216,10 @@ function createClient(clientOptions) {
209
216
  }
210
217
  }
211
218
  }
212
- const contentLength = response.headers.get("Content-Length");
213
- if (response.status === 204 || request.method === "HEAD" || contentLength === "0" && !response.headers.get("Transfer-Encoding")?.includes("chunked")) {
214
- return response.ok ? { data: void 0, response } : { error: void 0, response };
215
- }
216
- if (response.ok) {
217
- return { data: await readSuccessBody(response, parseAs), response };
218
- }
219
- return { error: await readErrorBody(response), response };
219
+ return readResult(response, parseAs, request.method);
220
220
  }
221
221
  return {
222
+ baseUrl,
222
223
  request(method, url, init) {
223
224
  return coreFetch(url, { ...init, method: method.toUpperCase() });
224
225
  },
@@ -364,6 +365,20 @@ function parseJSON(raw, response) {
364
365
  throw new SyntaxError(`${subject} is not JSON (${raw.length} characters); ${hint}`);
365
366
  }
366
367
  }
368
+ function hasEmptyBody(response, method) {
369
+ const isDeclaredEmpty = response.headers.get("Content-Length") === "0" && !response.headers.get("Transfer-Encoding")?.includes("chunked");
370
+ return method === "HEAD" || isDeclaredEmpty;
371
+ }
372
+ async function readResult(response, parseAs, method) {
373
+ const isEmpty = hasEmptyBody(response, method);
374
+ if (!response.ok) {
375
+ return { error: isEmpty ? void 0 : await readErrorBody(response), response };
376
+ }
377
+ if (parseAs === "json" && isEmpty) {
378
+ return { data: void 0, response };
379
+ }
380
+ return { data: await readSuccessBody(response, parseAs), response };
381
+ }
367
382
  async function readSuccessBody(response, parseAs) {
368
383
  if (parseAs === "stream") {
369
384
  return response.body;
@@ -395,7 +410,27 @@ function isDate(value) {
395
410
  function valueText(value) {
396
411
  return isDate(value) ? value.toISOString() : value;
397
412
  }
413
+ const HEADER_PARAM_STYLE = { style: "simple", explode: false, allowReserved: true };
414
+ function headerParamValue(value) {
415
+ if (value === null || typeof value !== "object" || isDate(value)) {
416
+ return valueText(value);
417
+ }
418
+ const serialize = Array.isArray(value) ? serializeArrayParam : serializeObjectParam;
419
+ return serialize("", value, HEADER_PARAM_STYLE) || void 0;
420
+ }
421
+ function serializeHeaderParams(header) {
422
+ if (!header || typeof header !== "object") {
423
+ return header;
424
+ }
425
+ const entries = Object.entries(header).map(([name, value]) => [name, headerParamValue(value)]);
426
+ return Object.fromEntries(entries);
427
+ }
398
428
  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
+ );
433
+ }
399
434
  const text = valueText(value);
400
435
  return allowReserved === true ? text : encodeURIComponent(text);
401
436
  }
@@ -403,11 +438,6 @@ function serializePrimitiveParam(name, value, options) {
403
438
  if (value === void 0 || value === null) {
404
439
  return "";
405
440
  }
406
- if (typeof value === "object" && !isDate(value)) {
407
- throw new Error(
408
- "Deeply-nested arrays/objects aren\u2019t supported. Provide your own `querySerializer()` to handle these."
409
- );
410
- }
411
441
  return `${name}=${encodeValue(value, options?.allowReserved)}`;
412
442
  }
413
443
  function joinExploded(values, style) {
@@ -418,7 +448,10 @@ function joinExploded(values, style) {
418
448
  function serializeUnexplodedObject(name, value, options) {
419
449
  const values = [];
420
450
  for (const k in value) {
421
- values.push(encodeValue(k), encodeValue(value[k], options.allowReserved));
451
+ values.push(
452
+ encodeValue(k, options.allowReserved),
453
+ encodeValue(value[k], options.allowReserved)
454
+ );
422
455
  }
423
456
  const final = values.join(",");
424
457
  switch (options.style) {
@@ -492,17 +525,15 @@ function serializeQueryParam(name, value, options) {
492
525
  const allowReserved = options?.allowReserved || false;
493
526
  if (Array.isArray(value)) {
494
527
  return serializeArrayParam(name, value, {
495
- style: "form",
496
- explode: true,
497
- ...options?.array,
528
+ style: options?.array?.style ?? "form",
529
+ explode: options?.array?.explode ?? true,
498
530
  allowReserved
499
531
  });
500
532
  }
501
533
  if (value && typeof value === "object" && !isDate(value)) {
502
534
  return serializeObjectParam(name, value, {
503
- style: "deepObject",
504
- explode: true,
505
- ...options?.object,
535
+ style: options?.object?.style ?? "deepObject",
536
+ explode: options?.object?.explode ?? true,
506
537
  allowReserved
507
538
  });
508
539
  }
@@ -522,6 +553,15 @@ function createQuerySerializer(options) {
522
553
  return search.join("&");
523
554
  };
524
555
  }
556
+ function mergeQueryOptions(clientOptions, requestOptions) {
557
+ const base = typeof clientOptions === "function" ? {} : clientOptions ?? {};
558
+ return {
559
+ ...base,
560
+ ...requestOptions,
561
+ array: { ...base.array, ...requestOptions.array },
562
+ object: { ...base.object, ...requestOptions.object }
563
+ };
564
+ }
525
565
  function defaultPathSerializer(pathname, pathParams) {
526
566
  const matches = pathname.match(PATH_PARAM_RE);
527
567
  if (!matches) {
@@ -576,17 +616,41 @@ function checkSerializedPath(template, url) {
576
616
  throw new Error(`${problem} path segment in "${name}"`);
577
617
  }
578
618
  }
579
- function isPlatformInstance(value, name) {
619
+ function typeTag(value) {
620
+ return Object.prototype.toString.call(value).slice(TYPE_TAG_START, TYPE_TAG_END);
621
+ }
622
+ function isClass(value, name, methods = []) {
580
623
  const type = globalThis[name];
581
- return typeof type === "function" && value instanceof type;
624
+ return typeof type === "function" && value instanceof type || typeTag(value) === name && methods.every((method) => typeof value[method] === "function");
625
+ }
626
+ function isError(value) {
627
+ return isClass(value, "Error");
628
+ }
629
+ function isFormData(value) {
630
+ return isClass(value, "FormData", FORM_DATA_METHODS);
631
+ }
632
+ function isBlob(value) {
633
+ return BLOB_CLASSES.some((name) => isClass(value, name, BLOB_METHODS));
634
+ }
635
+ function isReadableStream(value) {
636
+ return isClass(value, "ReadableStream", STREAM_METHODS);
637
+ }
638
+ function asFormDataOf(FormDataClass, form) {
639
+ if (typeof FormDataClass !== "function" || form instanceof FormDataClass) {
640
+ return form;
641
+ }
642
+ const copy = new FormDataClass();
643
+ for (const [name, value] of form.entries()) {
644
+ copy.append(name, value);
645
+ }
646
+ copy[CLIENT_BUILT_FORM] = true;
647
+ return copy;
582
648
  }
583
649
  function isStreamBody(body) {
584
- return isPlatformInstance(body, "ReadableStream") || typeof body?.[Symbol.asyncIterator] === "function";
650
+ return isReadableStream(body) || typeof body?.[Symbol.asyncIterator] === "function";
585
651
  }
586
652
  function isRawBody(body) {
587
- return ["FormData", "Blob", "URLSearchParams", "ArrayBuffer"].some(
588
- (name) => isPlatformInstance(body, name)
589
- ) || ArrayBuffer.isView(body) || isStreamBody(body);
653
+ return isFormData(body) || isBlob(body) || RAW_BODY_CLASSES.some((name) => isClass(body, name)) || ArrayBuffer.isView(body) || isStreamBody(body);
590
654
  }
591
655
  function isNestedFormValue(value) {
592
656
  return value && typeof value === "object" && !isDate(value);
@@ -615,7 +679,7 @@ function multipartBody(body) {
615
679
  formData[CLIENT_BUILT_FORM] = true;
616
680
  for (const [name, value] of Object.entries(body)) {
617
681
  for (const item of formItems(value)) {
618
- if (isPlatformInstance(item, "Blob")) {
682
+ if (isBlob(item)) {
619
683
  formData.append(name, item);
620
684
  } else if (isNestedFormValue(item)) {
621
685
  throw new Error(NESTED_FORM_VALUE_MESSAGE);
@@ -651,10 +715,14 @@ function createFinalURL(pathname, options) {
651
715
  if (search.startsWith("?")) {
652
716
  search = search.substring(1);
653
717
  }
654
- if (search) {
655
- finalURL += `${finalURL.includes("?") ? "&" : "?"}${search}`;
718
+ if (!search) {
719
+ return finalURL;
656
720
  }
657
- return finalURL;
721
+ const hashIndex = finalURL.indexOf("#");
722
+ const fragmentStart = hashIndex === -1 ? finalURL.length : hashIndex;
723
+ const beforeFragment = finalURL.slice(0, fragmentStart);
724
+ const joiner = beforeFragment.includes("?") ? "&" : "?";
725
+ return `${beforeFragment}${joiner}${search}${finalURL.slice(fragmentStart)}`;
658
726
  }
659
727
  function mergeHeaders(...allHeaders) {
660
728
  const finalHeaders = new Headers();