openapi-typesafe-fetch 1.0.0-rc.0 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -15,28 +15,59 @@ interface ClientOptions extends Omit<RequestInit, "headers"> {
15
15
  /** global pathSerializer */
16
16
  pathSerializer?: PathSerializer;
17
17
  headers?: HeadersOptions;
18
- /** RequestInit extension object to pass as 2nd argument to fetch when supported (defaults to undefined) */
18
+ /**
19
+ * RequestInit extension object to pass as 2nd argument to fetch when supported (defaults to
20
+ * undefined)
21
+ */
19
22
  requestInitExt?: Record<string, unknown>;
20
23
  }
21
24
 
25
+ /**
26
+ * `ClientOptions` for a Fetch API implementation with its own types, such as the `undici` package
27
+ * (upstream #2562). TypeScript cannot relate its `Request` and `Response` to the DOM lib's
28
+ * (`duplex`, `RequestDestination`, `BodyInit` differ), so only what the client needs is checked: a
29
+ * `fetch` that returns a promise and a `Request` class that takes a URL. Pass the `fetch` and the
30
+ * `Request` of the same implementation; middleware and results still use the DOM types. The
31
+ * `createClient` overload with `ClientOptions` comes first, so an inline `fetch` gets a typed
32
+ * `Request` input.
33
+ */
34
+ interface CustomFetchClientOptions extends Omit<ClientOptions, "fetch" | "Request"> {
35
+ /** `fetch` of another implementation (for example `fetch` from `undici`) */
36
+ fetch?: (input: never, init?: never) => PromiseLike<object>;
37
+ /** `Request` of the same implementation (for example `Request` from `undici`) */
38
+ Request?: new (
39
+ input: string,
40
+ init: never,
41
+ ) => object;
42
+ }
43
+
22
44
  type HeadersOptions =
23
45
  | Required<RequestInit>["headers"]
24
46
  | Record<string, string | number | boolean | (string | number | boolean)[] | null | undefined>;
25
47
 
26
48
  type QuerySerializer<T> = (
27
- query: T extends { parameters: any } ? NonNullable<T["parameters"]["query"]> : Record<string, unknown>,
49
+ // biome-ignore lint/suspicious/noExplicitAny: T["parameters"]["query"] needs an any constraint
50
+ query: T extends { parameters: any }
51
+ ? NonNullable<T["parameters"]["query"]>
52
+ : Record<string, unknown>,
28
53
  ) => string;
29
54
 
30
55
  /** @see https://swagger.io/docs/specification/serialization/#query */
31
56
  type QuerySerializerOptions = {
32
- /** Set serialization for arrays. @see https://swagger.io/docs/specification/serialization/#query */
57
+ /**
58
+ * Set serialization for arrays.
59
+ * @see https://swagger.io/docs/specification/serialization/#query
60
+ */
33
61
  array?: {
34
62
  /** default: "form" */
35
63
  style: "form" | "spaceDelimited" | "pipeDelimited";
36
64
  /** default: true */
37
65
  explode: boolean;
38
66
  };
39
- /** Set serialization for objects. @see https://swagger.io/docs/specification/serialization/#query */
67
+ /**
68
+ * Set serialization for objects.
69
+ * @see https://swagger.io/docs/specification/serialization/#query
70
+ */
40
71
  object?: {
41
72
  /** default: "deepObject" */
42
73
  style: "form" | "deepObject";
@@ -53,6 +84,8 @@ type QuerySerializerOptions = {
53
84
  allowReserved?: boolean;
54
85
  };
55
86
 
87
+ // The result is the fetch body; openapi-fetch types it as `any` (ADR-030).
88
+ // biome-ignore lint/suspicious/noExplicitAny: openapi-fetch's type, kept for parity
56
89
  type BodySerializer<T> = (body: OperationRequestBodyContent<T>) => any;
57
90
 
58
91
  type PathSerializer = (pathname: string, pathParams: Record<string, unknown>) => string;
@@ -78,25 +111,38 @@ interface DefaultParamsOption {
78
111
  }
79
112
 
80
113
  type ParamsOption<T> = T extends {
81
- parameters: any;
114
+ parameters: unknown;
82
115
  }
83
116
  ? RequiredKeysOf<T["parameters"]> extends never
84
117
  ? { params?: T["parameters"] }
85
118
  : { params: T["parameters"] }
86
119
  : DefaultParamsOption;
87
120
 
88
- // Writable<T> strips $Read markers (readOnly properties excluded from request body)
121
+ /** `true` for `any`: the operation of `createClient<any>()`, which says nothing about its body. */
122
+ type IsAny<T> = 0 extends 1 & T ? true : false;
123
+
124
+ // Writable<T> strips $Read markers (readOnly properties excluded from request body).
125
+ // An `any` operation is checked first: a conditional type on `any` takes both branches, which
126
+ // gave `{ body?: never } | { body: any }` and so a required init (upstream #2526).
89
127
  type RequestBodyOption<T> =
90
- Writable<OperationRequestBodyContent<T>> extends never
91
- ? { body?: never }
92
- : IsOperationRequestBodyOptional<T> extends true
93
- ? { body?: Writable<OperationRequestBodyContent<T>> }
94
- : { body: Writable<OperationRequestBodyContent<T>> };
128
+ IsAny<T> extends true
129
+ ? // biome-ignore lint/suspicious/noExplicitAny: an any schema takes any body (upstream #2526)
130
+ { body?: any }
131
+ : Writable<OperationRequestBodyContent<T>> extends never
132
+ ? { body?: never }
133
+ : IsOperationRequestBodyOptional<T> extends true
134
+ ? { body?: Writable<OperationRequestBodyContent<T>> }
135
+ : { body: Writable<OperationRequestBodyContent<T>> };
95
136
 
96
137
  type FetchOptions<T> = RequestOptions<T> & Omit<RequestInit, "body" | "headers">;
97
138
 
98
139
  // Readable<T> strips $Write markers (writeOnly properties excluded from response)
99
- type FetchResponse<T extends Record<string | number, any>, Options, Media extends MediaType> =
140
+ type FetchResponse<
141
+ // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
142
+ T extends Record<string | number, any>,
143
+ Options,
144
+ Media extends MediaType,
145
+ > =
100
146
  | {
101
147
  data: ParseAsResponse<Readable<SuccessResponse<ResponseObjectMap<T>, Media>>, Options>;
102
148
  error?: never;
@@ -145,7 +191,7 @@ interface MiddlewareCallbackParams {
145
191
  readonly params: MiddlewareRequestParams;
146
192
  /** Unique ID for this request */
147
193
  readonly id: string;
148
- /** createClient options (read-only) */
194
+ /** The options of this request after merging the client and request options (read-only) */
149
195
  readonly options: MergedOptions;
150
196
  }
151
197
 
@@ -176,29 +222,79 @@ type Middleware =
176
222
  onError: MiddlewareOnError;
177
223
  };
178
224
 
179
- /** This type helper makes the 2nd function param required if params/requestBody are required; otherwise, optional */
180
- type MaybeOptionalInit<Params, Location extends keyof Params> =
181
- RequiredKeysOf<FetchOptions<FilterKeys<Params, Location>>> extends never
182
- ? FetchOptions<FilterKeys<Params, Location>> | undefined
183
- : FetchOptions<FilterKeys<Params, Location>>;
225
+ /**
226
+ * This type helper makes the 2nd function param required if params/requestBody are required;
227
+ * otherwise, optional. `Location` is any key: with `Paths extends {}` a method is not known to be a
228
+ * key of `Paths[Path]` (upstream #2267). For a union of operations, the union of their inits: one
229
+ * deep type of every init gave TS2589 on hundreds of paths (upstream #1882).
230
+ */
231
+ type MaybeOptionalInit<Params, Location extends PropertyKey> = OperationInits<
232
+ FilterKeys<Params, Location>
233
+ >;
234
+
235
+ /**
236
+ * The inits of the operations, and `undefined` only when none of them requires anything. One
237
+ * operation's init is `undefined` too when nothing in it is required, as in openapi-fetch. For a
238
+ * union of operations (a union of paths, or `Parameters<typeof client.request>`), `undefined`
239
+ * would make the whole init optional, although a call to one of the paths sends the init it gets,
240
+ * and a path parameter left out stays `{id}` in the URL.
241
+ * One conditional type, as openapi-fetch's, not a union with a conditional `undefined`: while the
242
+ * operation is generic, a union made `FetchResponse` distribute over its members, so the `data` of
243
+ * a generic wrapper's call no longer related to `FetchResponse<Paths[Path][Method], Init, Media>`.
244
+ */
245
+ type OperationInits<Operation> =
246
+ RequiredKeysOf<OperationFetchOptions<Operation>> extends never
247
+ ? OperationFetchOptions<Operation> | undefined
248
+ : OperationFetchOptions<Operation>;
249
+
250
+ /**
251
+ * Each operation's init. A distributive alias, not `extends infer Operation`: while the path is
252
+ * not known, TypeScript reads an `infer` type as `unknown`, so the init lost its type as the
253
+ * contextual type of an argument and a literal such as `"available"` widened to `string`
254
+ * (swr-openapi's `Exact<Init>` checks).
255
+ */
256
+ type OperationFetchOptions<Operation> = Operation extends unknown ? FetchOptions<Operation> : never;
184
257
 
185
258
  // The final init param to accept.
186
259
  // - Determines if the param is optional or not.
187
260
  // - Performs arbitrary [key: string] addition.
188
- // Note: the addition MUST happen after all the inference happens (otherwise TS can’t infer if init is required or not).
261
+ // Note: the addition MUST happen after all the inference happens (otherwise TS can’t infer if
262
+ // init is required or not).
263
+ // - The condition is openapi-fetch's. TypeScript relates two conditional types only when their
264
+ // `extends` types are the same and their check types relate, so a client type written against
265
+ // openapi-fetch's `InitParam` (a wrapper, a cast) accepted this client only with this condition.
266
+ // A union of inits is required through `MaybeOptionalInit`, which then leaves out `undefined`.
267
+ // - The addition is a named alias: written out in both branches, TypeScript gave `never` for
268
+ // `Parameters` of the methods.
189
269
  type InitParam<Init> =
190
- RequiredKeysOf<Init> extends never ? [(Init & { [key: string]: unknown })?] : [Init & { [key: string]: unknown }];
270
+ RequiredKeysOf<Init> extends never ? [WithAnyKeys<Init>?] : [WithAnyKeys<Init>];
191
271
 
192
- type ClientMethod<
193
- Paths extends Record<string, Record<HttpMethod, {}>>,
194
- Method extends HttpMethod,
195
- Media extends MediaType,
196
- > = <Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>>(
272
+ /** The init with any other key (`[key: string]: unknown`), added after inference. */
273
+ type WithAnyKeys<Init> = Init & { [key: string]: unknown };
274
+
275
+ /**
276
+ * @internal Exported for openapi-typesafe-react-query only; not part of the API (ADR-034).
277
+ * The operation of `Method` on `Path`. Not `Paths[Path][Method]`: `Paths` is only `{}`, because
278
+ * the generated `interface paths` has no index signature and failed a `Record<string, …>`
279
+ * constraint with TS2344 (upstream #2267), so TypeScript does not know `Method` is a key. For a
280
+ * union of paths or methods, the union of their operations; a method a path lacks (`put?: never`)
281
+ * adds none.
282
+ */
283
+ type PathOperation<Paths, Path, Method> = Extract<
284
+ FilterKeys<Paths[Path & keyof Paths], Method>,
285
+ // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
286
+ Record<string | number, any>
287
+ >;
288
+
289
+ type ClientMethod<Paths extends {}, Method extends HttpMethod, Media extends MediaType> = <
290
+ Path extends PathsWithMethod<Paths, Method>,
291
+ Init extends MaybeOptionalInit<Paths[Path], Method>,
292
+ >(
197
293
  url: Path,
198
294
  ...init: InitParam<Init>
199
- ) => Promise<FetchResponse<Paths[Path][Method], Init, Media>>;
295
+ ) => Promise<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>;
200
296
 
201
- type ClientRequestMethod<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
297
+ type ClientRequestMethod<Paths extends {}, Media extends MediaType> = <
202
298
  Method extends HttpMethod,
203
299
  Path extends PathsWithMethod<Paths, Method>,
204
300
  Init extends MaybeOptionalInit<Paths[Path], Method>,
@@ -206,11 +302,21 @@ type ClientRequestMethod<Paths extends Record<string, Record<HttpMethod, {}>>, M
206
302
  method: Method,
207
303
  url: Path,
208
304
  ...init: InitParam<Init>
209
- ) => Promise<FetchResponse<Paths[Path][Method], Init, Media>>;
305
+ ) => Promise<FetchResponse<PathOperation<Paths, Path, Method>, Init, Media>>;
210
306
 
211
307
  // Only HTTP method keys become calls: `parameters` and `additionalOperations` are not methods.
212
- type ClientForPath<PathInfo extends Record<string | number, any>, Media extends MediaType> = {
213
- [Method in keyof PathInfo as Method extends HttpMethod ? Uppercase<Method> : never]: <
308
+ // They are left out by `Pick`, which keeps the `?` of a method, not by the key remapping: that is
309
+ // openapi-fetch's, and TypeScript relates two generic mapped types only when their key remappings
310
+ // are the same, so a path-based client type written like openapi-fetch's failed a cast (TS2352).
311
+ type ClientForPath<
312
+ // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
313
+ PathInfo extends Record<string | number, any>,
314
+ Media extends MediaType,
315
+ > = PathMethodCalls<Pick<PathInfo, HttpMethod & keyof PathInfo>, Media>;
316
+
317
+ // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
318
+ type PathMethodCalls<PathInfo extends Record<string | number, any>, Media extends MediaType> = {
319
+ [Method in keyof PathInfo as Uppercase<string & Method>]: <
214
320
  Init extends MaybeOptionalInit<PathInfo, Method>,
215
321
  >(
216
322
  ...init: InitParam<Init>
@@ -243,34 +349,56 @@ interface Client<Paths extends {}, Media extends MediaType = MediaType> {
243
349
  eject(...middleware: Middleware[]): void;
244
350
  }
245
351
 
246
- type ClientPathsWithMethod<CreatedClient extends Client<any, any>, Method extends HttpMethod> =
352
+ type ClientPathsWithMethod<
353
+ // biome-ignore lint/suspicious/noExplicitAny: matches a client of any paths, as in openapi-fetch
354
+ CreatedClient extends Client<any, any>,
355
+ Method extends HttpMethod,
356
+ > =
247
357
  CreatedClient extends Client<infer Paths, infer _Media> ? PathsWithMethod<Paths, Method> : never;
248
358
 
249
359
  type MethodResponse<
360
+ // biome-ignore lint/suspicious/noExplicitAny: matches a client of any paths, as in openapi-fetch
250
361
  CreatedClient extends Client<any, any>,
251
362
  Method extends HttpMethod,
252
363
  Path extends ClientPathsWithMethod<CreatedClient, Method>,
253
364
  Options = {},
254
365
  > =
255
- CreatedClient extends Client<infer Paths extends { [key: string]: any }, infer Media extends MediaType>
366
+ CreatedClient extends Client<
367
+ // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
368
+ infer Paths extends { [key: string]: any },
369
+ infer Media extends MediaType
370
+ >
256
371
  ? NonNullable<FetchResponse<Paths[Path][Method], Options, Media>["data"]>
257
372
  : never;
258
373
 
259
374
  declare function createClient<Paths extends {}, Media extends MediaType = MediaType>(
260
375
  clientOptions?: ClientOptions,
261
376
  ): Client<Paths, Media>;
377
+ declare function createClient<Paths extends {}, Media extends MediaType = MediaType>(
378
+ clientOptions?: CustomFetchClientOptions,
379
+ ): Client<Paths, Media>;
262
380
 
263
- type PathBasedClient<Paths extends Record<string | number, any>, Media extends MediaType = MediaType> = {
381
+ type PathBasedClient<
382
+ // biome-ignore lint/suspicious/noExplicitAny: unknown rejects the generated interfaces
383
+ Paths extends Record<string | number, any>,
384
+ Media extends MediaType = MediaType,
385
+ > = {
264
386
  [Path in keyof Paths]: ClientForPath<Paths[Path], Media>;
265
387
  };
266
388
 
267
- declare function wrapAsPathBasedClient<Paths extends {}, Media extends MediaType = MediaType>(
268
- client: Client<Paths, Media>,
269
- ): PathBasedClient<Paths, Media>;
389
+ declare function wrapAsPathBasedClient<
390
+ Paths extends {},
391
+ Media extends MediaType = MediaType,
392
+ >(client: Client<Paths, Media>): PathBasedClient<Paths, Media>;
270
393
 
271
- declare function createPathBasedClient<Paths extends {}, Media extends MediaType = MediaType>(
272
- clientOptions?: ClientOptions,
273
- ): PathBasedClient<Paths, Media>;
394
+ declare function createPathBasedClient<
395
+ Paths extends {},
396
+ Media extends MediaType = MediaType,
397
+ >(clientOptions?: ClientOptions): PathBasedClient<Paths, Media>;
398
+ declare function createPathBasedClient<
399
+ Paths extends {},
400
+ Media extends MediaType = MediaType,
401
+ >(clientOptions?: CustomFetchClientOptions): PathBasedClient<Paths, Media>;
274
402
 
275
403
  /** Serialize primitive params to string */
276
404
  declare function serializePrimitiveParam(
@@ -311,12 +439,85 @@ declare function createQuerySerializer<T = unknown>(
311
439
  * @type {import("./index.js").defaultPathSerializer}
312
440
  * @see https://swagger.io/docs/specification/serialization/#path
313
441
  */
314
- declare function defaultPathSerializer(pathname: string, pathParams: Record<string, unknown>): string;
442
+ declare function defaultPathSerializer(
443
+ pathname: string,
444
+ pathParams: Record<string, unknown>,
445
+ ): string;
446
+
447
+ /** Bodies that `fetch` sends as they are, so `defaultBodySerializer` returns them unchanged. */
448
+ type RawRequestBody = FormData | Blob | URLSearchParams | ArrayBuffer | ArrayBufferView;
449
+
450
+ /**
451
+ * The result of `defaultBodySerializer` for a body of type `Body`: the body itself when it is raw,
452
+ * `undefined` for `undefined` (`JSON.stringify` gives it back), `ObjectResult` for an object, and a
453
+ * string otherwise. `unknown` (and `any`) may be any of these.
454
+ */
455
+ type SerializedBody<Body, ObjectResult> = unknown extends Body
456
+ ? RawRequestBody | string | FormData | undefined
457
+ : Body extends RawRequestBody
458
+ ? Body
459
+ : Body extends undefined
460
+ ? undefined
461
+ : Body extends object
462
+ ? ObjectResult
463
+ : string;
464
+
465
+ /** The `Content-Type` values that make `defaultBodySerializer` turn an object into `FormData`. */
466
+ type MultipartMediaType = "multipart/form-data" | `multipart/form-data;${string}`;
467
+
468
+ /** `Headers` drops the spaces and tabs in front of a value before the media type is read. */
469
+ type TrimmedStart<Text extends string> = Text extends ` ${infer Rest}` | `\t${infer Rest}`
470
+ ? TrimmedStart<Rest>
471
+ : Text;
472
+
473
+ /**
474
+ * The `Content-Type` of headers written as a record, in any letter case of the name: `never` when
475
+ * the record has none, `unknown` when the type does not tell (`Headers`, pairs, a string record).
476
+ */
477
+ type ContentTypeOf<GivenHeaders> = GivenHeaders extends undefined
478
+ ? never
479
+ : GivenHeaders extends readonly unknown[] | { get(name: string): unknown }
480
+ ? unknown
481
+ : string extends keyof GivenHeaders
482
+ ? unknown
483
+ : ContentTypeEntries<GivenHeaders>[keyof GivenHeaders];
484
+
485
+ /** For each entry of a header record: its value when it is the `Content-Type`, else `never`. */
486
+ type ContentTypeEntries<Record> = { [Name in keyof Record]: ContentTypeEntry<Name, Record[Name]> };
487
+
488
+ /** The value of a header record entry when its name is `Content-Type` in any letter case. */
489
+ type ContentTypeEntry<Name, Value> =
490
+ Lowercase<Name & string> extends "content-type" ? Value : never;
491
+
492
+ /**
493
+ * What an object becomes with a `Content-Type` of type `ContentType`: `FormData` for a multipart
494
+ * one, a string for any other one or for none, and either when the type is not a literal.
495
+ */
496
+ type ObjectBodyResult<ContentType> = [ContentType] extends [never]
497
+ ? string
498
+ : ContentType extends null | undefined
499
+ ? string
500
+ : ContentType extends string
501
+ ? string extends ContentType
502
+ ? string | FormData
503
+ : Lowercase<TrimmedStart<ContentType>> extends MultipartMediaType
504
+ ? FormData
505
+ : string
506
+ : string | FormData;
315
507
 
316
508
  /**
317
- * Serialize body object to string; with a form content type in `headers`, as URL-encoded form data.
509
+ * Serialize a request body. A raw body (`FormData`, `Blob`, `URLSearchParams`, `ArrayBuffer`, a
510
+ * typed array) is returned as it is. With `headers`: a form `Content-Type` gives URL-encoded form
511
+ * data, a `multipart/form-data` one turns an object into `FormData`, and another non-JSON one keeps
512
+ * a string as it is. Anything else is JSON. Without `headers` an object can only become JSON.
513
+ * Headers written as a record literal tell the result type of an object body: `FormData` for a
514
+ * multipart `Content-Type`, `string` for another one or none. Other headers: `string | FormData`.
318
515
  */
319
- declare function defaultBodySerializer<T>(body: T, headers?: HeadersOptions): string;
516
+ declare function defaultBodySerializer<T>(body: T): SerializedBody<T, string>;
517
+ declare function defaultBodySerializer<
518
+ T,
519
+ const GivenHeaders extends HeadersOptions | undefined,
520
+ >(body: T, headers: GivenHeaders): SerializedBody<T, ObjectBodyResult<ContentTypeOf<GivenHeaders>>>;
320
521
 
321
522
  /** Construct URL string from baseUrl and handle path and query params */
322
523
  declare function createFinalURL<O>(
@@ -338,7 +539,5 @@ declare function mergeHeaders(...allHeaders: (HeadersOptions | undefined)[]): He
338
539
  /** Remove trailing slash from url */
339
540
  declare function removeTrailingSlash(url: string): string;
340
541
 
341
- // @ts-ignore
342
- export = createClient;
343
- export { createFinalURL, createPathBasedClient, createQuerySerializer, defaultBodySerializer, defaultPathSerializer, mergeHeaders, removeTrailingSlash, serializeArrayParam, serializeObjectParam, serializePrimitiveParam, wrapAsPathBasedClient };
344
- export type { BodySerializer, Client, ClientForPath, ClientMethod, ClientOptions, ClientPathsWithMethod, ClientRequestMethod, DefaultParamsOption, FetchOptions, FetchResponse, HeadersOptions, MaybeOptionalInit, MergedOptions, MethodResponse, Middleware, MiddlewareCallbackParams, MiddlewareRequestParams, ParamsOption, ParseAs, ParseAsResponse, PathBasedClient, PathSerializer, QuerySerializer, QuerySerializerOptions, RequestBodyOption, RequestOptions };
542
+ export { createFinalURL, createPathBasedClient, createQuerySerializer, createClient as default, defaultBodySerializer, defaultPathSerializer, mergeHeaders, removeTrailingSlash, serializeArrayParam, serializeObjectParam, serializePrimitiveParam, wrapAsPathBasedClient };
543
+ export type { BodySerializer, Client, ClientForPath, ClientMethod, ClientOptions, ClientPathsWithMethod, ClientRequestMethod, CustomFetchClientOptions, DefaultParamsOption, FetchOptions, FetchResponse, HeadersOptions, MaybeOptionalInit, MergedOptions, MethodResponse, Middleware, MiddlewareCallbackParams, MiddlewareRequestParams, ParamsOption, ParseAs, ParseAsResponse, PathBasedClient, PathOperation, PathSerializer, QuerySerializer, QuerySerializerOptions, RequestBodyOption, RequestOptions };
package/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  const PATH_PARAM_RE = /\{[^{}]+\}/g;
2
2
  const DOT_SEGMENT_RE = /(^|\/)\.\.?(\/|$)/;
3
+ const QUERY_OR_FRAGMENT_RE = /[?#]/;
3
4
  const JSON_MEDIA_TYPE_RE = /json/i;
4
- const FORM_MEDIA_TYPE = "application/x-www-form-urlencoded";
5
+ const FORM_MEDIA_TYPE_RE = /^application\/x-www-form-urlencoded(;|$)/i;
5
6
  const MULTIPART_MEDIA_TYPE_RE = /^multipart\/form-data(;|$)/i;
6
7
  const NESTED_FORM_VALUE_MESSAGE = "Nested form values need your own `bodySerializer()`.";
7
8
  const BOUNDARY_PARAMETER_RE = /;\s*boundary=/i;
@@ -57,11 +58,12 @@ function createClient(clientOptions) {
57
58
  const pathSerializer = requestPathSerializer || globalPathSerializer || defaultPathSerializer;
58
59
  const serializedBody = body === void 0 ? void 0 : bodySerializer(
59
60
  body,
60
- // Note: we declare mergeHeaders() both here and below because it’s a bit of a chicken-or-egg situation:
61
- // bodySerializer() needs all headers so we aren’t dropping ones set by the user, however,
62
- // the result of this ALSO sets the lowest-priority content-type header. So we re-merge below,
63
- // setting the content-type at the very beginning to be overwritten.
64
- // Lastly, based on the way headers work, it’s not a simple “present-or-not” check becauase null intentionally un-sets headers.
61
+ // Note: we declare mergeHeaders() both here and below because it’s a bit of a
62
+ // chicken-or-egg situation: bodySerializer() needs all headers so we aren’t dropping
63
+ // ones set by the user, however, the result of this ALSO sets the lowest-priority
64
+ // content-type header. So we re-merge below, setting the content-type at the very
65
+ // beginning to be overwritten. Lastly, based on the way headers work, it’s not a simple
66
+ // “present-or-not” check because null intentionally un-sets headers.
65
67
  mergeHeaders(baseHeaders, headers, params.header)
66
68
  );
67
69
  const finalHeaders = mergeHeaders(
@@ -88,7 +90,12 @@ function createClient(clientOptions) {
88
90
  let id;
89
91
  let options;
90
92
  let request = new Request(
91
- createFinalURL(schemaPath, { baseUrl: finalBaseUrl, params, querySerializer, pathSerializer }),
93
+ createFinalURL(schemaPath, {
94
+ baseUrl: finalBaseUrl,
95
+ params,
96
+ querySerializer,
97
+ pathSerializer
98
+ }),
92
99
  requestInit
93
100
  );
94
101
  let response;
@@ -117,13 +124,15 @@ function createClient(clientOptions) {
117
124
  id
118
125
  });
119
126
  if (result) {
120
- if (result instanceof Request) {
127
+ if (isRequest(result)) {
121
128
  request = result;
122
- } else if (result instanceof Response) {
129
+ } else if (isResponse(result)) {
123
130
  response = result;
124
131
  break;
125
132
  } else {
126
- throw new Error("onRequest: must return new Request() or Response() when modifying the request");
133
+ throw new Error(
134
+ "onRequest: must return new Request() or Response() when modifying the request"
135
+ );
127
136
  }
128
137
  }
129
138
  }
@@ -147,7 +156,7 @@ function createClient(clientOptions) {
147
156
  id
148
157
  });
149
158
  if (result) {
150
- if (result instanceof Response) {
159
+ if (isResponse(result)) {
151
160
  errorAfterMiddleware = void 0;
152
161
  response = result;
153
162
  break;
@@ -178,8 +187,10 @@ function createClient(clientOptions) {
178
187
  id
179
188
  });
180
189
  if (result) {
181
- if (!(result instanceof Response)) {
182
- throw new Error("onResponse: must return new Response() when modifying the response");
190
+ if (!isResponse(result)) {
191
+ throw new Error(
192
+ "onResponse: must return new Response() when modifying the response"
193
+ );
183
194
  }
184
195
  response = result;
185
196
  }
@@ -262,7 +273,9 @@ function createClient(clientOptions) {
262
273
  continue;
263
274
  }
264
275
  if (typeof m !== "object" || !("onRequest" in m || "onResponse" in m || "onError" in m)) {
265
- throw new Error("Middleware must be an object with one of `onRequest()`, `onResponse()` or `onError()`");
276
+ throw new Error(
277
+ "Middleware must be an object with one of `onRequest()`, `onResponse()` or `onError()`"
278
+ );
266
279
  }
267
280
  globalMiddlewares.push(m);
268
281
  }
@@ -335,6 +348,15 @@ function wrapAsPathBasedClient(coreClient) {
335
348
  function createPathBasedClient(clientOptions) {
336
349
  return wrapAsPathBasedClient(createClient(clientOptions));
337
350
  }
351
+ function isHeaders(value) {
352
+ return typeof value?.get === "function";
353
+ }
354
+ function isRequest(value) {
355
+ return typeof value?.method === "string" && typeof value.url === "string" && isHeaders(value.headers);
356
+ }
357
+ function isResponse(value) {
358
+ return typeof value?.status === "number" && isHeaders(value.headers);
359
+ }
338
360
  const KEPT_NAME_ESCAPES = /%(?:5B|5D|24|3A|40|2C)/g;
339
361
  function encodeName(name) {
340
362
  return encodeURIComponent(name).replace(KEPT_NAME_ESCAPES, decodeURIComponent);
@@ -362,7 +384,10 @@ function serializeObjectParam(name, value, options) {
362
384
  }[options.style] || "&";
363
385
  if (options.style !== "deepObject" && options.explode === false) {
364
386
  for (const k in value) {
365
- values.push(encodeName(k), options.allowReserved === true ? value[k] : encodeURIComponent(value[k]));
387
+ values.push(
388
+ encodeName(k),
389
+ options.allowReserved === true ? value[k] : encodeURIComponent(value[k])
390
+ );
366
391
  }
367
392
  const final2 = values.join(",");
368
393
  switch (options.style) {
@@ -484,35 +509,44 @@ function defaultPathSerializer(pathname, pathParams) {
484
509
  continue;
485
510
  }
486
511
  const value = pathParams[name];
512
+ let serialized;
487
513
  if (Array.isArray(value)) {
488
- nextURL = nextURL.replace(match, serializeArrayParam(name, value, { style, explode }));
489
- continue;
490
- }
491
- if (typeof value === "object") {
492
- nextURL = nextURL.replace(match, serializeObjectParam(name, value, { style, explode }));
493
- continue;
494
- }
495
- if (style === "matrix") {
496
- nextURL = nextURL.replace(match, `;${serializePrimitiveParam(name, value)}`);
497
- continue;
514
+ serialized = serializeArrayParam(name, value, { style, explode });
515
+ } else if (typeof value === "object") {
516
+ serialized = serializeObjectParam(name, value, { style, explode });
517
+ } else if (style === "matrix") {
518
+ serialized = `;${serializePrimitiveParam(name, value)}`;
519
+ } else {
520
+ serialized = style === "label" ? `.${encodeURIComponent(value)}` : encodeURIComponent(value);
498
521
  }
499
- nextURL = nextURL.replace(match, style === "label" ? `.${encodeURIComponent(value)}` : encodeURIComponent(value));
522
+ nextURL = nextURL.replace(match, () => serialized);
500
523
  }
501
- if (DOT_SEGMENT_RE.test(nextURL)) {
524
+ const path = nextURL.split(QUERY_OR_FRAGMENT_RE, 1)[0];
525
+ if (DOT_SEGMENT_RE.test(path)) {
502
526
  throw new Error(`Dot path segment in ${nextURL}`);
503
527
  }
528
+ if (path.startsWith("//")) {
529
+ throw new Error(`Empty first path segment in ${nextURL}`);
530
+ }
504
531
  return nextURL;
505
532
  }
506
533
  function isRawBody(body) {
507
534
  return body instanceof FormData || body instanceof Blob || body instanceof URLSearchParams || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
508
535
  }
509
536
  function formBody(body) {
510
- for (const value of Object.values(body)) {
537
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
538
+ return new URLSearchParams(body).toString();
539
+ }
540
+ const form = new URLSearchParams();
541
+ for (const [name, value] of Object.entries(body)) {
511
542
  if (value && typeof value === "object" && !Array.isArray(value)) {
512
543
  throw new Error(NESTED_FORM_VALUE_MESSAGE);
513
544
  }
545
+ if (value != null) {
546
+ form.append(name, value);
547
+ }
514
548
  }
515
- return new URLSearchParams(body).toString();
549
+ return form.toString();
516
550
  }
517
551
  function multipartBody(body) {
518
552
  const formData = new FormData();
@@ -533,12 +567,8 @@ function defaultBodySerializer(body, headers) {
533
567
  if (isRawBody(body)) {
534
568
  return body;
535
569
  }
536
- const contentType = headers ? (
537
- // `typeof`, not `instanceof Function`: headers from another realm (iframe, `node:vm`) have a
538
- // `get` that is not an instance of this realm's Function (upstream #2637).
539
- typeof headers.get === "function" ? headers.get("Content-Type") ?? headers.get("content-type") : headers["Content-Type"] ?? headers["content-type"]
540
- ) : void 0;
541
- if (contentType === FORM_MEDIA_TYPE) {
570
+ const contentType = headers && (isHeaders(headers) ? headers : mergeHeaders(headers)).get("Content-Type");
571
+ if (FORM_MEDIA_TYPE_RE.test(contentType)) {
542
572
  return formBody(body);
543
573
  }
544
574
  if (body && typeof body === "object" && MULTIPART_MEDIA_TYPE_RE.test(contentType)) {
@@ -569,7 +599,7 @@ function mergeHeaders(...allHeaders) {
569
599
  if (!h || typeof h !== "object") {
570
600
  continue;
571
601
  }
572
- const iterator = h instanceof Headers ? h.entries() : Object.entries(h);
602
+ const iterator = isHeaders(h) ? h.entries() : Symbol.iterator in h ? new Headers(h).entries() : Object.entries(h);
573
603
  for (const [k, v] of iterator) {
574
604
  if (v === null) {
575
605
  finalHeaders.delete(k);