openapi-fetch
Advanced tools
Comparing version 0.9.0-rc.0 to 0.9.0
# openapi-fetch | ||
## 0.9.0 | ||
### Minor Changes | ||
- [#1521](https://github.com/drwpow/openapi-typescript/pull/1521) [`b174dd6`](https://github.com/drwpow/openapi-typescript/commit/b174dd6a7668e2f1f6bf6bd086ba2dabf7fb669e) Thanks [@drwpow](https://github.com/drwpow)! - Add middleware support | ||
- [#1521](https://github.com/drwpow/openapi-typescript/pull/1521) [`fc3a468`](https://github.com/drwpow/openapi-typescript/commit/fc3a468c4342e17d203712be358b30a3fb82ab1e) Thanks [@drwpow](https://github.com/drwpow)! - ⚠️ Breaking change (internal): fetch() is now called with new Request() to support middleware (which may affect test mocking) | ||
- [#1521](https://github.com/drwpow/openapi-typescript/pull/1521) [`2551e4b`](https://github.com/drwpow/openapi-typescript/commit/2551e4bde41d5437a76c13bb5ba25ede4f14db10) Thanks [@drwpow](https://github.com/drwpow)! - ⚠️ **Breaking change**: Responses are no longer automatically `.clone()`’d in certain instances. Be sure to `.clone()` yourself if you need to access the raw body! | ||
- [#1534](https://github.com/drwpow/openapi-typescript/pull/1534) [`2bbeb92`](https://github.com/drwpow/openapi-typescript/commit/2bbeb92244cb82a534abb016ffb5fbd1255d9db5) Thanks [@drwpow](https://github.com/drwpow)! - ⚠️ Breaking change: no longer supports deeply-nested objects/arrays for query & path serialization. | ||
### Patch Changes | ||
- [#1484](https://github.com/drwpow/openapi-typescript/pull/1484) [`49bbd72`](https://github.com/drwpow/openapi-typescript/commit/49bbd72800f7bc6c460a741c50d11eb216746290) Thanks [@drwpow](https://github.com/drwpow)! - Remove prepare script | ||
- [#1479](https://github.com/drwpow/openapi-typescript/pull/1479) [`c6d945b`](https://github.com/drwpow/openapi-typescript/commit/c6d945be717bb3999178fb3a77292e41e1b7ab80) Thanks [@darwish](https://github.com/darwish)! - Fixed build of openapi-typescript-helpers for CommonJS environments | ||
- [#1534](https://github.com/drwpow/openapi-typescript/pull/1534) [`2bbeb92`](https://github.com/drwpow/openapi-typescript/commit/2bbeb92244cb82a534abb016ffb5fbd1255d9db5) Thanks [@drwpow](https://github.com/drwpow)! - Add support for automatic label & matrix path serialization. | ||
- [#1521](https://github.com/drwpow/openapi-typescript/pull/1521) [`fd44bd2`](https://github.com/drwpow/openapi-typescript/commit/fd44bd28d881715e30f5a71435f05f6bae13859d) Thanks [@drwpow](https://github.com/drwpow)! - Support arrays in headers | ||
- [#1534](https://github.com/drwpow/openapi-typescript/pull/1534) [`2bbeb92`](https://github.com/drwpow/openapi-typescript/commit/2bbeb92244cb82a534abb016ffb5fbd1255d9db5) Thanks [@drwpow](https://github.com/drwpow)! - Remove leading question marks from querySerializer | ||
- [#1530](https://github.com/drwpow/openapi-typescript/pull/1530) [`4765658`](https://github.com/drwpow/openapi-typescript/commit/4765658460e0850d005e3f08cd63c4949326349b) Thanks [@wydengyre](https://github.com/wydengyre)! - Exports the ClientMethod utility type. | ||
- Updated dependencies [[`c6d945b`](https://github.com/drwpow/openapi-typescript/commit/c6d945be717bb3999178fb3a77292e41e1b7ab80)]: | ||
- openapi-typescript-helpers@0.0.7 | ||
## 0.8.2 | ||
@@ -4,0 +33,0 @@ |
@@ -23,3 +23,3 @@ import type { | ||
/** global querySerializer */ | ||
querySerializer?: QuerySerializer<unknown>; | ||
querySerializer?: QuerySerializer<unknown> | QuerySerializerOptions; | ||
/** global bodySerializer */ | ||
@@ -48,2 +48,28 @@ bodySerializer?: BodySerializer<unknown>; | ||
/** @see https://swagger.io/docs/specification/serialization/#query */ | ||
export type QuerySerializerOptions = { | ||
/** Set serialization for arrays. @see https://swagger.io/docs/specification/serialization/#query */ | ||
array?: { | ||
/** default: "form" */ | ||
style: "form" | "spaceDelimited" | "pipeDelimited"; | ||
/** default: true */ | ||
explode: boolean; | ||
}; | ||
/** Set serialization for objects. @see https://swagger.io/docs/specification/serialization/#query */ | ||
object?: { | ||
/** default: "deepObject" */ | ||
style: "form" | "deepObject"; | ||
/** default: true */ | ||
explode: boolean; | ||
}; | ||
/** | ||
* The `allowReserved` keyword specifies whether the reserved characters | ||
* `:/?#[]@!$&'()*+,;=` in parameter values are allowed to be sent as they | ||
* are, or should be percent-encoded. By default, allowReserved is `false`, | ||
* and reserved characters are percent-encoded. | ||
* @see https://swagger.io/docs/specification/serialization/#query | ||
*/ | ||
allowReserved?: boolean; | ||
}; | ||
export type BodySerializer<T> = (body: OperationRequestBodyContent<T>) => any; | ||
@@ -112,3 +138,3 @@ | ||
RequestBodyOption<T> & { | ||
querySerializer?: QuerySerializer<T>; | ||
querySerializer?: QuerySerializer<T> | QuerySerializerOptions; | ||
bodySerializer?: BodySerializer<T>; | ||
@@ -154,2 +180,10 @@ parseAs?: ParseAs; | ||
export type ClientMethod<Paths extends {}, M> = < | ||
P extends PathsWithMethod<Paths, M>, | ||
I extends MaybeOptionalInit<Paths[P], M>, | ||
>( | ||
url: P, | ||
...init: I | ||
) => Promise<FetchResponse<Paths[P][M], I[0]>>; | ||
export default function createClient<Paths extends {}>( | ||
@@ -159,65 +193,17 @@ clientOptions?: ClientOptions, | ||
/** Call a GET endpoint */ | ||
GET< | ||
P extends PathsWithMethod<Paths, "get">, | ||
I extends MaybeOptionalInit<Paths[P], "get">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["get"], I[0]>>; | ||
GET: ClientMethod<Paths, "get">; | ||
/** Call a PUT endpoint */ | ||
PUT< | ||
P extends PathsWithMethod<Paths, "put">, | ||
I extends MaybeOptionalInit<Paths[P], "put">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["put"], I[0]>>; | ||
PUT: ClientMethod<Paths, "put">; | ||
/** Call a POST endpoint */ | ||
POST< | ||
P extends PathsWithMethod<Paths, "post">, | ||
I extends MaybeOptionalInit<Paths[P], "post">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["post"], I[0]>>; | ||
POST: ClientMethod<Paths, "post">; | ||
/** Call a DELETE endpoint */ | ||
DELETE< | ||
P extends PathsWithMethod<Paths, "delete">, | ||
I extends MaybeOptionalInit<Paths[P], "delete">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["delete"], I[0]>>; | ||
DELETE: ClientMethod<Paths, "delete">; | ||
/** Call a OPTIONS endpoint */ | ||
OPTIONS< | ||
P extends PathsWithMethod<Paths, "options">, | ||
I extends MaybeOptionalInit<Paths[P], "options">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["options"], I[0]>>; | ||
OPTIONS: ClientMethod<Paths, "options">; | ||
/** Call a HEAD endpoint */ | ||
HEAD< | ||
P extends PathsWithMethod<Paths, "head">, | ||
I extends MaybeOptionalInit<Paths[P], "head">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["head"], I[0]>>; | ||
HEAD: ClientMethod<Paths, "head">; | ||
/** Call a PATCH endpoint */ | ||
PATCH< | ||
P extends PathsWithMethod<Paths, "patch">, | ||
I extends MaybeOptionalInit<Paths[P], "patch">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["patch"], I[0]>>; | ||
PATCH: ClientMethod<Paths, "patch">; | ||
/** Call a TRACE endpoint */ | ||
TRACE< | ||
P extends PathsWithMethod<Paths, "trace">, | ||
I extends MaybeOptionalInit<Paths[P], "trace">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["trace"], I[0]>>; | ||
TRACE: ClientMethod<Paths, "trace">; | ||
/** Register middleware */ | ||
@@ -229,10 +215,51 @@ use(...middleware: Middleware[]): void; | ||
/** Serialize primitive params to string */ | ||
export declare function serializePrimitiveParam( | ||
name: string, | ||
value: string, | ||
options?: { allowReserved?: boolean }, | ||
): string; | ||
/** Serialize object param to string */ | ||
export declare function serializeObjectParam( | ||
name: string, | ||
value: Record<string, unknown>, | ||
options: { | ||
style: "simple" | "label" | "matrix" | "form" | "deepObject"; | ||
explode: boolean; | ||
allowReserved?: boolean; | ||
}, | ||
): string; | ||
/** Serialize array param to string */ | ||
export declare function serializeArrayParam( | ||
name: string, | ||
value: unknown[], | ||
options: { | ||
style: | ||
| "simple" | ||
| "label" | ||
| "matrix" | ||
| "form" | ||
| "spaceDelimited" | ||
| "pipeDelimited"; | ||
explode: boolean; | ||
allowReserved?: boolean; | ||
}, | ||
): string; | ||
/** Serialize query params to string */ | ||
export declare function defaultQuerySerializer<T = unknown>(q: T): string; | ||
export declare function createQuerySerializer<T = unknown>( | ||
options?: QuerySerializerOptions, | ||
): (queryParams: T) => string; | ||
/** Serialize query param schema types according to expected default OpenAPI 3.x behavior */ | ||
export declare function defaultQueryParamSerializer<T = unknown>( | ||
key: string[], | ||
value: T, | ||
): string | undefined; | ||
/** | ||
* Handle different OpenAPI 3.x serialization styles | ||
* @type {import("./index.js").defaultPathSerializer} | ||
* @see https://swagger.io/docs/specification/serialization/#path | ||
*/ | ||
export declare function defaultPathSerializer( | ||
pathname: string, | ||
pathParams: Record<string, unknown>, | ||
): string; | ||
@@ -239,0 +266,0 @@ /** Serialize body object to string */ |
@@ -6,2 +6,4 @@ // settings & const | ||
const PATH_PARAM_RE = /\{[^{}]+\}/g; | ||
/** | ||
@@ -15,4 +17,4 @@ * Create an openapi-fetch client. | ||
fetch: baseFetch = globalThis.fetch, | ||
querySerializer: globalQuerySerializer = defaultQuerySerializer, | ||
bodySerializer: globalBodySerializer = defaultBodySerializer, | ||
querySerializer: globalQuerySerializer, | ||
bodySerializer: globalBodySerializer, | ||
headers: baseHeaders, | ||
@@ -22,3 +24,3 @@ ...baseOptions | ||
if (baseUrl.endsWith("/")) { | ||
baseUrl = baseUrl.slice(0, -1); // remove trailing slash | ||
baseUrl = baseUrl.substring(0, baseUrl.length - 1); | ||
} | ||
@@ -39,7 +41,23 @@ baseHeaders = mergeHeaders(DEFAULT_HEADERS, baseHeaders); | ||
parseAs = "json", | ||
querySerializer = globalQuerySerializer, | ||
bodySerializer = globalBodySerializer, | ||
querySerializer: requestQuerySerializer, | ||
bodySerializer = globalBodySerializer ?? defaultBodySerializer, | ||
...init | ||
} = fetchOptions || {}; | ||
let querySerializer = | ||
typeof globalQuerySerializer === "function" | ||
? globalQuerySerializer | ||
: createQuerySerializer(globalQuerySerializer); | ||
if (requestQuerySerializer) { | ||
querySerializer = | ||
typeof requestQuerySerializer === "function" | ||
? requestQuerySerializer | ||
: createQuerySerializer({ | ||
...(typeof globalQuerySerializer === "object" | ||
? globalQuerySerializer | ||
: {}), | ||
...requestQuerySerializer, | ||
}); | ||
} | ||
const requestInit = { | ||
@@ -55,7 +73,3 @@ redirect: "follow", | ||
let request = new Request( | ||
createFinalURL(url, { | ||
baseUrl, | ||
params, | ||
querySerializer, | ||
}), | ||
createFinalURL(url, { baseUrl, params, querySerializer }), | ||
requestInit, | ||
@@ -201,73 +215,220 @@ ); | ||
/** | ||
* Serialize query params to string | ||
* @type {import("./index.js").defaultQuerySerializer} | ||
* Serialize primitive param values | ||
* @type {import("./index.js").serializePrimitiveParam} | ||
*/ | ||
export function defaultQuerySerializer(q) { | ||
const search = []; | ||
if (q && typeof q === "object") { | ||
for (const [k, v] of Object.entries(q)) { | ||
const value = defaultQueryParamSerializer([k], v); | ||
if (value) { | ||
search.push(value); | ||
export function serializePrimitiveParam(name, value, options) { | ||
if (value === undefined || value === null) { | ||
return ""; | ||
} | ||
if (typeof value === "object") { | ||
throw new Error( | ||
`Deeply-nested arrays/objects aren’t supported. Provide your own \`querySerializer()\` to handle these.`, | ||
); | ||
} | ||
return `${name}=${options?.allowReserved === true ? value : encodeURIComponent(value)}`; | ||
} | ||
/** | ||
* Serialize object param (shallow only) | ||
* @type {import("./index.js").serializeObjectParam} | ||
*/ | ||
export function serializeObjectParam(name, value, options) { | ||
if (!value || typeof value !== "object") { | ||
return ""; | ||
} | ||
const values = []; | ||
const joiner = | ||
{ | ||
simple: ",", | ||
label: ".", | ||
matrix: ";", | ||
}[options.style] || "&"; | ||
// explode: false | ||
if (options.style !== "deepObject" && options.explode === false) { | ||
for (const k in value) { | ||
values.push( | ||
k, | ||
options.allowReserved === true | ||
? value[k] | ||
: encodeURIComponent(value[k]), | ||
); | ||
} | ||
const final = values.join(","); // note: values are always joined by comma in explode: false (but joiner can prefix) | ||
switch (options.style) { | ||
case "form": { | ||
return `${name}=${final}`; | ||
} | ||
case "label": { | ||
return `.${final}`; | ||
} | ||
case "matrix": { | ||
return `;${name}=${final}`; | ||
} | ||
default: { | ||
return final; | ||
} | ||
} | ||
} | ||
return search.join("&"); | ||
// explode: true | ||
for (const k in value) { | ||
const finalName = options.style === "deepObject" ? `${name}[${k}]` : k; | ||
values.push(serializePrimitiveParam(finalName, value[k], options)); | ||
} | ||
const final = values.join(joiner); | ||
return options.style === "label" || options.style === "matrix" | ||
? `${joiner}${final}` | ||
: final; | ||
} | ||
/** | ||
* Serialize query param schema types according to expected default OpenAPI 3.x behavior | ||
* @type {import("./index.js").defaultQueryParamSerializer} | ||
* Serialize array param (shallow only) | ||
* @type {import("./index.js").serializeArrayParam} | ||
*/ | ||
export function defaultQueryParamSerializer(key, value) { | ||
if (value === null || value === undefined) { | ||
return undefined; | ||
export function serializeArrayParam(name, value, options) { | ||
if (!Array.isArray(value)) { | ||
return ""; | ||
} | ||
if (typeof value === "string") { | ||
return `${deepObjectPath(key)}=${encodeURIComponent(value)}`; | ||
} | ||
if (typeof value === "number" || typeof value === "boolean") { | ||
return `${deepObjectPath(key)}=${String(value)}`; | ||
} | ||
if (Array.isArray(value)) { | ||
if (!value.length) { | ||
return undefined; | ||
} | ||
const nextValue = []; | ||
for (const item of value) { | ||
const next = defaultQueryParamSerializer(key, item); | ||
if (next !== undefined) { | ||
nextValue.push(next); | ||
// explode: false | ||
if (options.explode === false) { | ||
const joiner = | ||
{ form: ",", spaceDelimited: "%20", pipeDelimited: "|" }[options.style] || | ||
","; // note: for arrays, joiners vary wildly based on style + explode behavior | ||
const final = ( | ||
options.allowReserved === true | ||
? value | ||
: value.map((v) => encodeURIComponent(v)) | ||
).join(joiner); | ||
switch (options.style) { | ||
case "simple": { | ||
return final; | ||
} | ||
case "label": { | ||
return `.${final}`; | ||
} | ||
case "matrix": { | ||
return `;${name}=${final}`; | ||
} | ||
case "spaceDelimited": | ||
case "pipeDelimited": | ||
default: { | ||
return `${name}=${final}`; | ||
} | ||
} | ||
return nextValue.join(`&`); | ||
} | ||
if (typeof value === "object") { | ||
if (!Object.keys(value).length) { | ||
return undefined; | ||
// explode: true | ||
const joiner = { simple: ",", label: ".", matrix: ";" }[options.style] || "&"; | ||
const values = []; | ||
for (const v of value) { | ||
if (options.style === "simple" || options.style === "label") { | ||
values.push(options.allowReserved === true ? v : encodeURIComponent(v)); | ||
} else { | ||
values.push(serializePrimitiveParam(name, v, options)); | ||
} | ||
const nextValue = []; | ||
for (const [k, v] of Object.entries(value)) { | ||
if (v !== undefined && v !== null) { | ||
const next = defaultQueryParamSerializer([...key, k], v); | ||
if (next !== undefined) { | ||
nextValue.push(next); | ||
} | ||
return options.style === "label" || options.style === "matrix" | ||
? `${joiner}${values.join(joiner)}` | ||
: values.join(joiner); | ||
} | ||
/** | ||
* Serialize query params to string | ||
* @type {import("./index.js").createQuerySerializer} | ||
*/ | ||
export function createQuerySerializer(options) { | ||
return function querySerializer(queryParams) { | ||
const search = []; | ||
if (queryParams && typeof queryParams === "object") { | ||
for (const name in queryParams) { | ||
const value = queryParams[name]; | ||
if (value === undefined || value === null) { | ||
continue; | ||
} | ||
if (Array.isArray(value)) { | ||
search.push( | ||
serializeArrayParam(name, value, { | ||
style: "form", | ||
explode: true, | ||
...options?.array, | ||
allowReserved: options?.allowReserved || false, | ||
}), | ||
); | ||
continue; | ||
} | ||
if (typeof value === "object") { | ||
search.push( | ||
serializeObjectParam(name, value, { | ||
style: "deepObject", | ||
explode: true, | ||
...options?.object, | ||
allowReserved: options?.allowReserved || false, | ||
}), | ||
); | ||
continue; | ||
} | ||
search.push(serializePrimitiveParam(name, value, options)); | ||
} | ||
} | ||
return nextValue.join("&"); | ||
} | ||
return encodeURIComponent(`${deepObjectPath(key)}=${String(value)}`); | ||
return search.join("&"); | ||
}; | ||
} | ||
/** | ||
* Flatten a node path into a deepObject string | ||
* @type {import("./index.js").deepObjectPath} | ||
* Handle different OpenAPI 3.x serialization styles | ||
* @type {import("./index.js").defaultPathSerializer} | ||
* @see https://swagger.io/docs/specification/serialization/#path | ||
*/ | ||
function deepObjectPath(path) { | ||
let output = path[0]; | ||
for (const k of path.slice(1)) { | ||
output += `[${k}]`; | ||
export function defaultPathSerializer(pathname, pathParams) { | ||
let nextURL = pathname; | ||
for (const match of pathname.match(PATH_PARAM_RE) ?? []) { | ||
let name = match.substring(1, match.length - 1); | ||
let explode = false; | ||
let style = "simple"; | ||
if (name.endsWith("*")) { | ||
explode = true; | ||
name = name.substring(0, name.length - 1); | ||
} | ||
if (name.startsWith(".")) { | ||
style = "label"; | ||
name = name.substring(1); | ||
} else if (name.startsWith(";")) { | ||
style = "matrix"; | ||
name = name.substring(1); | ||
} | ||
if ( | ||
!pathParams || | ||
pathParams[name] === undefined || | ||
pathParams[name] === null | ||
) { | ||
continue; | ||
} | ||
const value = pathParams[name]; | ||
if (Array.isArray(value)) { | ||
nextURL = nextURL.replace( | ||
match, | ||
serializeArrayParam(name, value, { style, explode }), | ||
); | ||
continue; | ||
} | ||
if (typeof value === "object") { | ||
nextURL = nextURL.replace( | ||
match, | ||
serializeObjectParam(name, value, { style, explode }), | ||
); | ||
continue; | ||
} | ||
if (style === "matrix") { | ||
nextURL = nextURL.replace( | ||
match, | ||
`;${serializePrimitiveParam(name, value)}`, | ||
); | ||
continue; | ||
} | ||
nextURL = nextURL.replace(match, style === "label" ? `.${value}` : value); | ||
continue; | ||
} | ||
return output; | ||
return nextURL; | ||
} | ||
@@ -289,8 +450,9 @@ | ||
let finalURL = `${options.baseUrl}${pathname}`; | ||
if (options.params.path) { | ||
for (const [k, v] of Object.entries(options.params.path)) { | ||
finalURL = finalURL.replace(`{${k}}`, encodeURIComponent(String(v))); | ||
} | ||
if (options.params?.path) { | ||
finalURL = defaultPathSerializer(finalURL, options.params.path); | ||
} | ||
const search = options.querySerializer(options.params.query ?? {}); | ||
let search = options.querySerializer(options.params.query ?? {}); | ||
if (search.startsWith("?")) { | ||
search = search.substring(1); | ||
} | ||
if (search) { | ||
@@ -297,0 +459,0 @@ finalURL += `?${search}`; |
@@ -23,3 +23,3 @@ import type { | ||
/** global querySerializer */ | ||
querySerializer?: QuerySerializer<unknown>; | ||
querySerializer?: QuerySerializer<unknown> | QuerySerializerOptions; | ||
/** global bodySerializer */ | ||
@@ -48,2 +48,28 @@ bodySerializer?: BodySerializer<unknown>; | ||
/** @see https://swagger.io/docs/specification/serialization/#query */ | ||
export type QuerySerializerOptions = { | ||
/** Set serialization for arrays. @see https://swagger.io/docs/specification/serialization/#query */ | ||
array?: { | ||
/** default: "form" */ | ||
style: "form" | "spaceDelimited" | "pipeDelimited"; | ||
/** default: true */ | ||
explode: boolean; | ||
}; | ||
/** Set serialization for objects. @see https://swagger.io/docs/specification/serialization/#query */ | ||
object?: { | ||
/** default: "deepObject" */ | ||
style: "form" | "deepObject"; | ||
/** default: true */ | ||
explode: boolean; | ||
}; | ||
/** | ||
* The `allowReserved` keyword specifies whether the reserved characters | ||
* `:/?#[]@!$&'()*+,;=` in parameter values are allowed to be sent as they | ||
* are, or should be percent-encoded. By default, allowReserved is `false`, | ||
* and reserved characters are percent-encoded. | ||
* @see https://swagger.io/docs/specification/serialization/#query | ||
*/ | ||
allowReserved?: boolean; | ||
}; | ||
export type BodySerializer<T> = (body: OperationRequestBodyContent<T>) => any; | ||
@@ -112,3 +138,3 @@ | ||
RequestBodyOption<T> & { | ||
querySerializer?: QuerySerializer<T>; | ||
querySerializer?: QuerySerializer<T> | QuerySerializerOptions; | ||
bodySerializer?: BodySerializer<T>; | ||
@@ -154,2 +180,10 @@ parseAs?: ParseAs; | ||
export type ClientMethod<Paths extends {}, M> = < | ||
P extends PathsWithMethod<Paths, M>, | ||
I extends MaybeOptionalInit<Paths[P], M>, | ||
>( | ||
url: P, | ||
...init: I | ||
) => Promise<FetchResponse<Paths[P][M], I[0]>>; | ||
export default function createClient<Paths extends {}>( | ||
@@ -159,65 +193,17 @@ clientOptions?: ClientOptions, | ||
/** Call a GET endpoint */ | ||
GET< | ||
P extends PathsWithMethod<Paths, "get">, | ||
I extends MaybeOptionalInit<Paths[P], "get">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["get"], I[0]>>; | ||
GET: ClientMethod<Paths, "get">; | ||
/** Call a PUT endpoint */ | ||
PUT< | ||
P extends PathsWithMethod<Paths, "put">, | ||
I extends MaybeOptionalInit<Paths[P], "put">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["put"], I[0]>>; | ||
PUT: ClientMethod<Paths, "put">; | ||
/** Call a POST endpoint */ | ||
POST< | ||
P extends PathsWithMethod<Paths, "post">, | ||
I extends MaybeOptionalInit<Paths[P], "post">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["post"], I[0]>>; | ||
POST: ClientMethod<Paths, "post">; | ||
/** Call a DELETE endpoint */ | ||
DELETE< | ||
P extends PathsWithMethod<Paths, "delete">, | ||
I extends MaybeOptionalInit<Paths[P], "delete">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["delete"], I[0]>>; | ||
DELETE: ClientMethod<Paths, "delete">; | ||
/** Call a OPTIONS endpoint */ | ||
OPTIONS< | ||
P extends PathsWithMethod<Paths, "options">, | ||
I extends MaybeOptionalInit<Paths[P], "options">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["options"], I[0]>>; | ||
OPTIONS: ClientMethod<Paths, "options">; | ||
/** Call a HEAD endpoint */ | ||
HEAD< | ||
P extends PathsWithMethod<Paths, "head">, | ||
I extends MaybeOptionalInit<Paths[P], "head">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["head"], I[0]>>; | ||
HEAD: ClientMethod<Paths, "head">; | ||
/** Call a PATCH endpoint */ | ||
PATCH< | ||
P extends PathsWithMethod<Paths, "patch">, | ||
I extends MaybeOptionalInit<Paths[P], "patch">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["patch"], I[0]>>; | ||
PATCH: ClientMethod<Paths, "patch">; | ||
/** Call a TRACE endpoint */ | ||
TRACE< | ||
P extends PathsWithMethod<Paths, "trace">, | ||
I extends MaybeOptionalInit<Paths[P], "trace">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["trace"], I[0]>>; | ||
TRACE: ClientMethod<Paths, "trace">; | ||
/** Register middleware */ | ||
@@ -229,10 +215,51 @@ use(...middleware: Middleware[]): void; | ||
/** Serialize primitive params to string */ | ||
export declare function serializePrimitiveParam( | ||
name: string, | ||
value: string, | ||
options?: { allowReserved?: boolean }, | ||
): string; | ||
/** Serialize object param to string */ | ||
export declare function serializeObjectParam( | ||
name: string, | ||
value: Record<string, unknown>, | ||
options: { | ||
style: "simple" | "label" | "matrix" | "form" | "deepObject"; | ||
explode: boolean; | ||
allowReserved?: boolean; | ||
}, | ||
): string; | ||
/** Serialize array param to string */ | ||
export declare function serializeArrayParam( | ||
name: string, | ||
value: unknown[], | ||
options: { | ||
style: | ||
| "simple" | ||
| "label" | ||
| "matrix" | ||
| "form" | ||
| "spaceDelimited" | ||
| "pipeDelimited"; | ||
explode: boolean; | ||
allowReserved?: boolean; | ||
}, | ||
): string; | ||
/** Serialize query params to string */ | ||
export declare function defaultQuerySerializer<T = unknown>(q: T): string; | ||
export declare function createQuerySerializer<T = unknown>( | ||
options?: QuerySerializerOptions, | ||
): (queryParams: T) => string; | ||
/** Serialize query param schema types according to expected default OpenAPI 3.x behavior */ | ||
export declare function defaultQueryParamSerializer<T = unknown>( | ||
key: string[], | ||
value: T, | ||
): string | undefined; | ||
/** | ||
* Handle different OpenAPI 3.x serialization styles | ||
* @type {import("./index.js").defaultPathSerializer} | ||
* @see https://swagger.io/docs/specification/serialization/#path | ||
*/ | ||
export declare function defaultPathSerializer( | ||
pathname: string, | ||
pathParams: Record<string, unknown>, | ||
): string; | ||
@@ -239,0 +266,0 @@ /** Serialize body object to string */ |
@@ -1,1 +0,1 @@ | ||
var C={"Content-Type":"application/json"};function P(o){let{baseUrl:e="",fetch:n=globalThis.fetch,querySerializer:a=$,bodySerializer:i=z,headers:f,...m}={...o};e.endsWith("/")&&(e=e.slice(0,-1)),f=A(C,f);let l=[];async function c(r,t){let{fetch:h=n,headers:q,params:b={},parseAs:w="json",querySerializer:T=a,bodySerializer:g=i,...x}=t||{},y={redirect:"follow",...m,...x,headers:A(f,q,b.header)};y.body&&(y.body=g(y.body));let p=new Request(H(r,{baseUrl:e,params:b,querySerializer:T}),y);y.body instanceof FormData&&p.headers.delete("Content-Type");let O={baseUrl:e,fetch:h,parseAs:w,querySerializer:T,bodySerializer:g};for(let u of l)if(u&&typeof u=="object"&&typeof u.onRequest=="function"){p.schemaPath=r,p.params=b;let d=await u.onRequest(p,O);if(d){if(!(d instanceof Request))throw new Error("Middleware must return new Request() when modifying the request");p=d}}let s=await h(p);for(let u=l.length-1;u>=0;u--){let d=l[u];if(d&&typeof d=="object"&&typeof d.onResponse=="function"){let j=await d.onResponse(s,O);if(j){if(!(j instanceof Response))throw new Error("Middleware must return new Response() when modifying the response");s=j}}}if(s.status===204||s.headers.get("Content-Length")==="0")return s.ok?{data:{},response:s}:{error:{},response:s};if(s.ok)return w==="stream"?{data:s.body,response:s}:{data:await s[w](),response:s};let R={};try{R=await s.json()}catch{R=await s.text()}return{error:R,response:s}}return{async GET(r,t){return c(r,{...t,method:"GET"})},async PUT(r,t){return c(r,{...t,method:"PUT"})},async POST(r,t){return c(r,{...t,method:"POST"})},async DELETE(r,t){return c(r,{...t,method:"DELETE"})},async OPTIONS(r,t){return c(r,{...t,method:"OPTIONS"})},async HEAD(r,t){return c(r,{...t,method:"HEAD"})},async PATCH(r,t){return c(r,{...t,method:"PATCH"})},async TRACE(r,t){return c(r,{...t,method:"TRACE"})},use(...r){for(let t of r)if(t){if(typeof t!="object"||!("onRequest"in t||"onResponse"in t))throw new Error("Middleware must be an object with one of `onRequest()` or `onResponse()`");l.push(t)}},eject(...r){for(let t of r){let h=l.indexOf(t);h!==-1&&l.splice(h,1)}}}}function $(o){let e=[];if(o&&typeof o=="object")for(let[n,a]of Object.entries(o)){let i=E([n],a);i&&e.push(i)}return e.join("&")}function E(o,e){if(e!=null){if(typeof e=="string")return`${S(o)}=${encodeURIComponent(e)}`;if(typeof e=="number"||typeof e=="boolean")return`${S(o)}=${String(e)}`;if(Array.isArray(e)){if(!e.length)return;let n=[];for(let a of e){let i=E(o,a);i!==void 0&&n.push(i)}return n.join("&")}if(typeof e=="object"){if(!Object.keys(e).length)return;let n=[];for(let[a,i]of Object.entries(e))if(i!=null){let f=E([...o,a],i);f!==void 0&&n.push(f)}return n.join("&")}return encodeURIComponent(`${S(o)}=${String(e)}`)}}function S(o){let e=o[0];for(let n of o.slice(1))e+=`[${n}]`;return e}function z(o){return JSON.stringify(o)}function H(o,e){let n=`${e.baseUrl}${o}`;if(e.params.path)for(let[i,f]of Object.entries(e.params.path))n=n.replace(`{${i}}`,encodeURIComponent(String(f)));let a=e.querySerializer(e.params.query??{});return a&&(n+=`?${a}`),n}function A(...o){let e=new Headers;for(let n of o){if(!n||typeof n!="object")continue;let a=n instanceof Headers?n.entries():Object.entries(n);for(let[i,f]of a)if(f===null)e.delete(i);else if(Array.isArray(f))for(let m of f)e.append(i,m);else f!==void 0&&e.set(i,f)}return e}export{H as createFinalURL,P as default,z as defaultBodySerializer,E as defaultQueryParamSerializer,$ as defaultQuerySerializer,A as mergeHeaders}; | ||
var D={"Content-Type":"application/json"},U=/\{[^{}]+\}/g;function H(s){let{baseUrl:t="",fetch:e=globalThis.fetch,querySerializer:n,bodySerializer:r,headers:i,...a}={...s};t.endsWith("/")&&(t=t.substring(0,t.length-1)),i=T(D,i);let l=[];async function u(c,o){let{fetch:p=e,headers:O,params:R={},parseAs:j="json",querySerializer:b,bodySerializer:A=r??I,...C}=o||{},x=typeof n=="function"?n:S(n);b&&(x=typeof b=="function"?b:S({...typeof n=="object"?n:{},...b}));let m={redirect:"follow",...a,...C,headers:T(i,O,R.header)};m.body&&(m.body=A(m.body));let h=new Request(L(c,{baseUrl:t,params:R,querySerializer:x}),m);m.body instanceof FormData&&h.headers.delete("Content-Type");let E={baseUrl:t,fetch:p,parseAs:j,querySerializer:x,bodySerializer:A};for(let d of l)if(d&&typeof d=="object"&&typeof d.onRequest=="function"){h.schemaPath=c,h.params=R;let y=await d.onRequest(h,E);if(y){if(!(y instanceof Request))throw new Error("Middleware must return new Request() when modifying the request");h=y}}let f=await p(h);for(let d=l.length-1;d>=0;d--){let y=l[d];if(y&&typeof y=="object"&&typeof y.onResponse=="function"){let g=await y.onResponse(f,E);if(g){if(!(g instanceof Response))throw new Error("Middleware must return new Response() when modifying the response");f=g}}}if(f.status===204||f.headers.get("Content-Length")==="0")return f.ok?{data:{},response:f}:{error:{},response:f};if(f.ok)return j==="stream"?{data:f.body,response:f}:{data:await f[j](),response:f};let $={};try{$=await f.json()}catch{$=await f.text()}return{error:$,response:f}}return{async GET(c,o){return u(c,{...o,method:"GET"})},async PUT(c,o){return u(c,{...o,method:"PUT"})},async POST(c,o){return u(c,{...o,method:"POST"})},async DELETE(c,o){return u(c,{...o,method:"DELETE"})},async OPTIONS(c,o){return u(c,{...o,method:"OPTIONS"})},async HEAD(c,o){return u(c,{...o,method:"HEAD"})},async PATCH(c,o){return u(c,{...o,method:"PATCH"})},async TRACE(c,o){return u(c,{...o,method:"TRACE"})},use(...c){for(let o of c)if(o){if(typeof o!="object"||!("onRequest"in o||"onResponse"in o))throw new Error("Middleware must be an object with one of `onRequest()` or `onResponse()`");l.push(o)}},eject(...c){for(let o of c){let p=l.indexOf(o);p!==-1&&l.splice(p,1)}}}}function w(s,t,e){if(t==null)return"";if(typeof t=="object")throw new Error("Deeply-nested arrays/objects aren\u2019t supported. Provide your own `querySerializer()` to handle these.");return`${s}=${e?.allowReserved===!0?t:encodeURIComponent(t)}`}function z(s,t,e){if(!t||typeof t!="object")return"";let n=[],r={simple:",",label:".",matrix:";"}[e.style]||"&";if(e.style!=="deepObject"&&e.explode===!1){for(let l in t)n.push(l,e.allowReserved===!0?t[l]:encodeURIComponent(t[l]));let a=n.join(",");switch(e.style){case"form":return`${s}=${a}`;case"label":return`.${a}`;case"matrix":return`;${s}=${a}`;default:return a}}for(let a in t){let l=e.style==="deepObject"?`${s}[${a}]`:a;n.push(w(l,t[a],e))}let i=n.join(r);return e.style==="label"||e.style==="matrix"?`${r}${i}`:i}function q(s,t,e){if(!Array.isArray(t))return"";if(e.explode===!1){let i={form:",",spaceDelimited:"%20",pipeDelimited:"|"}[e.style]||",",a=(e.allowReserved===!0?t:t.map(l=>encodeURIComponent(l))).join(i);switch(e.style){case"simple":return a;case"label":return`.${a}`;case"matrix":return`;${s}=${a}`;case"spaceDelimited":case"pipeDelimited":default:return`${s}=${a}`}}let n={simple:",",label:".",matrix:";"}[e.style]||"&",r=[];for(let i of t)e.style==="simple"||e.style==="label"?r.push(e.allowReserved===!0?i:encodeURIComponent(i)):r.push(w(s,i,e));return e.style==="label"||e.style==="matrix"?`${n}${r.join(n)}`:r.join(n)}function S(s){return function(e){let n=[];if(e&&typeof e=="object")for(let r in e){let i=e[r];if(i!=null){if(Array.isArray(i)){n.push(q(r,i,{style:"form",explode:!0,...s?.array,allowReserved:s?.allowReserved||!1}));continue}if(typeof i=="object"){n.push(z(r,i,{style:"deepObject",explode:!0,...s?.object,allowReserved:s?.allowReserved||!1}));continue}n.push(w(r,i,s))}}return n.join("&")}}function P(s,t){let e=s;for(let n of s.match(U)??[]){let r=n.substring(1,n.length-1),i=!1,a="simple";if(r.endsWith("*")&&(i=!0,r=r.substring(0,r.length-1)),r.startsWith(".")?(a="label",r=r.substring(1)):r.startsWith(";")&&(a="matrix",r=r.substring(1)),!t||t[r]===void 0||t[r]===null)continue;let l=t[r];if(Array.isArray(l)){e=e.replace(n,q(r,l,{style:a,explode:i}));continue}if(typeof l=="object"){e=e.replace(n,z(r,l,{style:a,explode:i}));continue}if(a==="matrix"){e=e.replace(n,`;${w(r,l)}`);continue}e=e.replace(n,a==="label"?`.${l}`:l)}return e}function I(s){return JSON.stringify(s)}function L(s,t){let e=`${t.baseUrl}${s}`;t.params?.path&&(e=P(e,t.params.path));let n=t.querySerializer(t.params.query??{});return n.startsWith("?")&&(n=n.substring(1)),n&&(e+=`?${n}`),e}function T(...s){let t=new Headers;for(let e of s){if(!e||typeof e!="object")continue;let n=e instanceof Headers?e.entries():Object.entries(e);for(let[r,i]of n)if(i===null)t.delete(r);else if(Array.isArray(i))for(let a of i)t.append(r,a);else i!==void 0&&t.set(r,i)}return t}export{L as createFinalURL,S as createQuerySerializer,H as default,I as defaultBodySerializer,P as defaultPathSerializer,T as mergeHeaders,q as serializeArrayParam,z as serializeObjectParam,w as serializePrimitiveParam}; |
{ | ||
"name": "openapi-fetch", | ||
"description": "Fast, typesafe fetch client for your OpenAPI schema. Only 2kb (min). Works with React, Vue, Svelte, or vanilla JS.", | ||
"version": "0.9.0-rc.0", | ||
"description": "Fast, typesafe fetch client for your OpenAPI schema. Only 5 kb (min). Works with React, Vue, Svelte, or vanilla JS.", | ||
"version": "0.9.0", | ||
"author": { | ||
@@ -66,3 +66,3 @@ "name": "Drew Powers", | ||
"dependencies": { | ||
"openapi-typescript-helpers": "^0.0.6" | ||
"openapi-typescript-helpers": "^0.0.7" | ||
}, | ||
@@ -69,0 +69,0 @@ "devDependencies": { |
<img src="../../docs/public/assets/openapi-fetch.svg" alt="openapi-fetch" width="216" height="40" /> | ||
openapi-fetch is a typesafe fetch client that pulls in your OpenAPI schema. Weighs **2 kB** and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS. | ||
openapi-fetch is a typesafe fetch client that pulls in your OpenAPI schema. Weighs **4 kB** and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS. | ||
| Library | Size (min) | “GET” request | | ||
| :------------------------- | ---------: | :------------------------ | | ||
| openapi-fetch | `2 kB` | `151k` ops/s (fastest) | | ||
| openapi-typescript-fetch | `4 kB` | `99k` ops/s (1.4× slower) | | ||
| axios | `32 kB` | `90k` ops/s (1.6× slower) | | ||
| superagent | `55 kB` | `42k` ops/s (3× slower) | | ||
| openapi-typescript-codegen | `367 kB` | `71k` ops/s (2× slower) | | ||
| Library | Size (min) | “GET” request | | ||
| :------------------------- | ---------: | :------------------------- | | ||
| openapi-fetch | `5 kB` | `278k` ops/s (fastest) | | ||
| openapi-typescript-fetch | `4 kB` | `130k` ops/s (2.1× slower) | | ||
| axios | `32 kB` | `217k` ops/s (1.3× slower) | | ||
| superagent | `55 kB` | `63k` ops/s (4.4× slower) | | ||
| openapi-typescript-codegen | `367 kB` | `106k` ops/s (2.6× slower) | | ||
@@ -48,3 +48,3 @@ The syntax is inspired by popular libraries like react-query or Apollo client, but without all the bells and whistles and in a 2 kB package. | ||
- ✅ Also eliminates `as` type overrides that can also hide bugs | ||
- ✅ All of this in a **2 kB** client package 🎉 | ||
- ✅ All of this in a **5 kb** client package 🎉 | ||
@@ -51,0 +51,0 @@ ## 🔧 Setup |
@@ -23,3 +23,3 @@ import type { | ||
/** global querySerializer */ | ||
querySerializer?: QuerySerializer<unknown>; | ||
querySerializer?: QuerySerializer<unknown> | QuerySerializerOptions; | ||
/** global bodySerializer */ | ||
@@ -48,2 +48,28 @@ bodySerializer?: BodySerializer<unknown>; | ||
/** @see https://swagger.io/docs/specification/serialization/#query */ | ||
export type QuerySerializerOptions = { | ||
/** Set serialization for arrays. @see https://swagger.io/docs/specification/serialization/#query */ | ||
array?: { | ||
/** default: "form" */ | ||
style: "form" | "spaceDelimited" | "pipeDelimited"; | ||
/** default: true */ | ||
explode: boolean; | ||
}; | ||
/** Set serialization for objects. @see https://swagger.io/docs/specification/serialization/#query */ | ||
object?: { | ||
/** default: "deepObject" */ | ||
style: "form" | "deepObject"; | ||
/** default: true */ | ||
explode: boolean; | ||
}; | ||
/** | ||
* The `allowReserved` keyword specifies whether the reserved characters | ||
* `:/?#[]@!$&'()*+,;=` in parameter values are allowed to be sent as they | ||
* are, or should be percent-encoded. By default, allowReserved is `false`, | ||
* and reserved characters are percent-encoded. | ||
* @see https://swagger.io/docs/specification/serialization/#query | ||
*/ | ||
allowReserved?: boolean; | ||
}; | ||
export type BodySerializer<T> = (body: OperationRequestBodyContent<T>) => any; | ||
@@ -112,3 +138,3 @@ | ||
RequestBodyOption<T> & { | ||
querySerializer?: QuerySerializer<T>; | ||
querySerializer?: QuerySerializer<T> | QuerySerializerOptions; | ||
bodySerializer?: BodySerializer<T>; | ||
@@ -154,2 +180,10 @@ parseAs?: ParseAs; | ||
export type ClientMethod<Paths extends {}, M> = < | ||
P extends PathsWithMethod<Paths, M>, | ||
I extends MaybeOptionalInit<Paths[P], M>, | ||
>( | ||
url: P, | ||
...init: I | ||
) => Promise<FetchResponse<Paths[P][M], I[0]>>; | ||
export default function createClient<Paths extends {}>( | ||
@@ -159,65 +193,17 @@ clientOptions?: ClientOptions, | ||
/** Call a GET endpoint */ | ||
GET< | ||
P extends PathsWithMethod<Paths, "get">, | ||
I extends MaybeOptionalInit<Paths[P], "get">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["get"], I[0]>>; | ||
GET: ClientMethod<Paths, "get">; | ||
/** Call a PUT endpoint */ | ||
PUT< | ||
P extends PathsWithMethod<Paths, "put">, | ||
I extends MaybeOptionalInit<Paths[P], "put">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["put"], I[0]>>; | ||
PUT: ClientMethod<Paths, "put">; | ||
/** Call a POST endpoint */ | ||
POST< | ||
P extends PathsWithMethod<Paths, "post">, | ||
I extends MaybeOptionalInit<Paths[P], "post">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["post"], I[0]>>; | ||
POST: ClientMethod<Paths, "post">; | ||
/** Call a DELETE endpoint */ | ||
DELETE< | ||
P extends PathsWithMethod<Paths, "delete">, | ||
I extends MaybeOptionalInit<Paths[P], "delete">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["delete"], I[0]>>; | ||
DELETE: ClientMethod<Paths, "delete">; | ||
/** Call a OPTIONS endpoint */ | ||
OPTIONS< | ||
P extends PathsWithMethod<Paths, "options">, | ||
I extends MaybeOptionalInit<Paths[P], "options">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["options"], I[0]>>; | ||
OPTIONS: ClientMethod<Paths, "options">; | ||
/** Call a HEAD endpoint */ | ||
HEAD< | ||
P extends PathsWithMethod<Paths, "head">, | ||
I extends MaybeOptionalInit<Paths[P], "head">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["head"], I[0]>>; | ||
HEAD: ClientMethod<Paths, "head">; | ||
/** Call a PATCH endpoint */ | ||
PATCH< | ||
P extends PathsWithMethod<Paths, "patch">, | ||
I extends MaybeOptionalInit<Paths[P], "patch">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["patch"], I[0]>>; | ||
PATCH: ClientMethod<Paths, "patch">; | ||
/** Call a TRACE endpoint */ | ||
TRACE< | ||
P extends PathsWithMethod<Paths, "trace">, | ||
I extends MaybeOptionalInit<Paths[P], "trace">, | ||
>( | ||
url: P, | ||
...init: I | ||
): Promise<FetchResponse<Paths[P]["trace"], I[0]>>; | ||
TRACE: ClientMethod<Paths, "trace">; | ||
/** Register middleware */ | ||
@@ -229,10 +215,51 @@ use(...middleware: Middleware[]): void; | ||
/** Serialize primitive params to string */ | ||
export declare function serializePrimitiveParam( | ||
name: string, | ||
value: string, | ||
options?: { allowReserved?: boolean }, | ||
): string; | ||
/** Serialize object param to string */ | ||
export declare function serializeObjectParam( | ||
name: string, | ||
value: Record<string, unknown>, | ||
options: { | ||
style: "simple" | "label" | "matrix" | "form" | "deepObject"; | ||
explode: boolean; | ||
allowReserved?: boolean; | ||
}, | ||
): string; | ||
/** Serialize array param to string */ | ||
export declare function serializeArrayParam( | ||
name: string, | ||
value: unknown[], | ||
options: { | ||
style: | ||
| "simple" | ||
| "label" | ||
| "matrix" | ||
| "form" | ||
| "spaceDelimited" | ||
| "pipeDelimited"; | ||
explode: boolean; | ||
allowReserved?: boolean; | ||
}, | ||
): string; | ||
/** Serialize query params to string */ | ||
export declare function defaultQuerySerializer<T = unknown>(q: T): string; | ||
export declare function createQuerySerializer<T = unknown>( | ||
options?: QuerySerializerOptions, | ||
): (queryParams: T) => string; | ||
/** Serialize query param schema types according to expected default OpenAPI 3.x behavior */ | ||
export declare function defaultQueryParamSerializer<T = unknown>( | ||
key: string[], | ||
value: T, | ||
): string | undefined; | ||
/** | ||
* Handle different OpenAPI 3.x serialization styles | ||
* @type {import("./index.js").defaultPathSerializer} | ||
* @see https://swagger.io/docs/specification/serialization/#path | ||
*/ | ||
export declare function defaultPathSerializer( | ||
pathname: string, | ||
pathParams: Record<string, unknown>, | ||
): string; | ||
@@ -239,0 +266,0 @@ /** Serialize body object to string */ |
292
src/index.js
@@ -6,2 +6,4 @@ // settings & const | ||
const PATH_PARAM_RE = /\{[^{}]+\}/g; | ||
/** | ||
@@ -15,4 +17,4 @@ * Create an openapi-fetch client. | ||
fetch: baseFetch = globalThis.fetch, | ||
querySerializer: globalQuerySerializer = defaultQuerySerializer, | ||
bodySerializer: globalBodySerializer = defaultBodySerializer, | ||
querySerializer: globalQuerySerializer, | ||
bodySerializer: globalBodySerializer, | ||
headers: baseHeaders, | ||
@@ -22,3 +24,3 @@ ...baseOptions | ||
if (baseUrl.endsWith("/")) { | ||
baseUrl = baseUrl.slice(0, -1); // remove trailing slash | ||
baseUrl = baseUrl.substring(0, baseUrl.length - 1); | ||
} | ||
@@ -39,7 +41,23 @@ baseHeaders = mergeHeaders(DEFAULT_HEADERS, baseHeaders); | ||
parseAs = "json", | ||
querySerializer = globalQuerySerializer, | ||
bodySerializer = globalBodySerializer, | ||
querySerializer: requestQuerySerializer, | ||
bodySerializer = globalBodySerializer ?? defaultBodySerializer, | ||
...init | ||
} = fetchOptions || {}; | ||
let querySerializer = | ||
typeof globalQuerySerializer === "function" | ||
? globalQuerySerializer | ||
: createQuerySerializer(globalQuerySerializer); | ||
if (requestQuerySerializer) { | ||
querySerializer = | ||
typeof requestQuerySerializer === "function" | ||
? requestQuerySerializer | ||
: createQuerySerializer({ | ||
...(typeof globalQuerySerializer === "object" | ||
? globalQuerySerializer | ||
: {}), | ||
...requestQuerySerializer, | ||
}); | ||
} | ||
const requestInit = { | ||
@@ -55,7 +73,3 @@ redirect: "follow", | ||
let request = new Request( | ||
createFinalURL(url, { | ||
baseUrl, | ||
params, | ||
querySerializer, | ||
}), | ||
createFinalURL(url, { baseUrl, params, querySerializer }), | ||
requestInit, | ||
@@ -201,73 +215,220 @@ ); | ||
/** | ||
* Serialize query params to string | ||
* @type {import("./index.js").defaultQuerySerializer} | ||
* Serialize primitive param values | ||
* @type {import("./index.js").serializePrimitiveParam} | ||
*/ | ||
export function defaultQuerySerializer(q) { | ||
const search = []; | ||
if (q && typeof q === "object") { | ||
for (const [k, v] of Object.entries(q)) { | ||
const value = defaultQueryParamSerializer([k], v); | ||
if (value) { | ||
search.push(value); | ||
export function serializePrimitiveParam(name, value, options) { | ||
if (value === undefined || value === null) { | ||
return ""; | ||
} | ||
if (typeof value === "object") { | ||
throw new Error( | ||
`Deeply-nested arrays/objects aren’t supported. Provide your own \`querySerializer()\` to handle these.`, | ||
); | ||
} | ||
return `${name}=${options?.allowReserved === true ? value : encodeURIComponent(value)}`; | ||
} | ||
/** | ||
* Serialize object param (shallow only) | ||
* @type {import("./index.js").serializeObjectParam} | ||
*/ | ||
export function serializeObjectParam(name, value, options) { | ||
if (!value || typeof value !== "object") { | ||
return ""; | ||
} | ||
const values = []; | ||
const joiner = | ||
{ | ||
simple: ",", | ||
label: ".", | ||
matrix: ";", | ||
}[options.style] || "&"; | ||
// explode: false | ||
if (options.style !== "deepObject" && options.explode === false) { | ||
for (const k in value) { | ||
values.push( | ||
k, | ||
options.allowReserved === true | ||
? value[k] | ||
: encodeURIComponent(value[k]), | ||
); | ||
} | ||
const final = values.join(","); // note: values are always joined by comma in explode: false (but joiner can prefix) | ||
switch (options.style) { | ||
case "form": { | ||
return `${name}=${final}`; | ||
} | ||
case "label": { | ||
return `.${final}`; | ||
} | ||
case "matrix": { | ||
return `;${name}=${final}`; | ||
} | ||
default: { | ||
return final; | ||
} | ||
} | ||
} | ||
return search.join("&"); | ||
// explode: true | ||
for (const k in value) { | ||
const finalName = options.style === "deepObject" ? `${name}[${k}]` : k; | ||
values.push(serializePrimitiveParam(finalName, value[k], options)); | ||
} | ||
const final = values.join(joiner); | ||
return options.style === "label" || options.style === "matrix" | ||
? `${joiner}${final}` | ||
: final; | ||
} | ||
/** | ||
* Serialize query param schema types according to expected default OpenAPI 3.x behavior | ||
* @type {import("./index.js").defaultQueryParamSerializer} | ||
* Serialize array param (shallow only) | ||
* @type {import("./index.js").serializeArrayParam} | ||
*/ | ||
export function defaultQueryParamSerializer(key, value) { | ||
if (value === null || value === undefined) { | ||
return undefined; | ||
export function serializeArrayParam(name, value, options) { | ||
if (!Array.isArray(value)) { | ||
return ""; | ||
} | ||
if (typeof value === "string") { | ||
return `${deepObjectPath(key)}=${encodeURIComponent(value)}`; | ||
} | ||
if (typeof value === "number" || typeof value === "boolean") { | ||
return `${deepObjectPath(key)}=${String(value)}`; | ||
} | ||
if (Array.isArray(value)) { | ||
if (!value.length) { | ||
return undefined; | ||
} | ||
const nextValue = []; | ||
for (const item of value) { | ||
const next = defaultQueryParamSerializer(key, item); | ||
if (next !== undefined) { | ||
nextValue.push(next); | ||
// explode: false | ||
if (options.explode === false) { | ||
const joiner = | ||
{ form: ",", spaceDelimited: "%20", pipeDelimited: "|" }[options.style] || | ||
","; // note: for arrays, joiners vary wildly based on style + explode behavior | ||
const final = ( | ||
options.allowReserved === true | ||
? value | ||
: value.map((v) => encodeURIComponent(v)) | ||
).join(joiner); | ||
switch (options.style) { | ||
case "simple": { | ||
return final; | ||
} | ||
case "label": { | ||
return `.${final}`; | ||
} | ||
case "matrix": { | ||
return `;${name}=${final}`; | ||
} | ||
case "spaceDelimited": | ||
case "pipeDelimited": | ||
default: { | ||
return `${name}=${final}`; | ||
} | ||
} | ||
return nextValue.join(`&`); | ||
} | ||
if (typeof value === "object") { | ||
if (!Object.keys(value).length) { | ||
return undefined; | ||
// explode: true | ||
const joiner = { simple: ",", label: ".", matrix: ";" }[options.style] || "&"; | ||
const values = []; | ||
for (const v of value) { | ||
if (options.style === "simple" || options.style === "label") { | ||
values.push(options.allowReserved === true ? v : encodeURIComponent(v)); | ||
} else { | ||
values.push(serializePrimitiveParam(name, v, options)); | ||
} | ||
const nextValue = []; | ||
for (const [k, v] of Object.entries(value)) { | ||
if (v !== undefined && v !== null) { | ||
const next = defaultQueryParamSerializer([...key, k], v); | ||
if (next !== undefined) { | ||
nextValue.push(next); | ||
} | ||
return options.style === "label" || options.style === "matrix" | ||
? `${joiner}${values.join(joiner)}` | ||
: values.join(joiner); | ||
} | ||
/** | ||
* Serialize query params to string | ||
* @type {import("./index.js").createQuerySerializer} | ||
*/ | ||
export function createQuerySerializer(options) { | ||
return function querySerializer(queryParams) { | ||
const search = []; | ||
if (queryParams && typeof queryParams === "object") { | ||
for (const name in queryParams) { | ||
const value = queryParams[name]; | ||
if (value === undefined || value === null) { | ||
continue; | ||
} | ||
if (Array.isArray(value)) { | ||
search.push( | ||
serializeArrayParam(name, value, { | ||
style: "form", | ||
explode: true, | ||
...options?.array, | ||
allowReserved: options?.allowReserved || false, | ||
}), | ||
); | ||
continue; | ||
} | ||
if (typeof value === "object") { | ||
search.push( | ||
serializeObjectParam(name, value, { | ||
style: "deepObject", | ||
explode: true, | ||
...options?.object, | ||
allowReserved: options?.allowReserved || false, | ||
}), | ||
); | ||
continue; | ||
} | ||
search.push(serializePrimitiveParam(name, value, options)); | ||
} | ||
} | ||
return nextValue.join("&"); | ||
} | ||
return encodeURIComponent(`${deepObjectPath(key)}=${String(value)}`); | ||
return search.join("&"); | ||
}; | ||
} | ||
/** | ||
* Flatten a node path into a deepObject string | ||
* @type {import("./index.js").deepObjectPath} | ||
* Handle different OpenAPI 3.x serialization styles | ||
* @type {import("./index.js").defaultPathSerializer} | ||
* @see https://swagger.io/docs/specification/serialization/#path | ||
*/ | ||
function deepObjectPath(path) { | ||
let output = path[0]; | ||
for (const k of path.slice(1)) { | ||
output += `[${k}]`; | ||
export function defaultPathSerializer(pathname, pathParams) { | ||
let nextURL = pathname; | ||
for (const match of pathname.match(PATH_PARAM_RE) ?? []) { | ||
let name = match.substring(1, match.length - 1); | ||
let explode = false; | ||
let style = "simple"; | ||
if (name.endsWith("*")) { | ||
explode = true; | ||
name = name.substring(0, name.length - 1); | ||
} | ||
if (name.startsWith(".")) { | ||
style = "label"; | ||
name = name.substring(1); | ||
} else if (name.startsWith(";")) { | ||
style = "matrix"; | ||
name = name.substring(1); | ||
} | ||
if ( | ||
!pathParams || | ||
pathParams[name] === undefined || | ||
pathParams[name] === null | ||
) { | ||
continue; | ||
} | ||
const value = pathParams[name]; | ||
if (Array.isArray(value)) { | ||
nextURL = nextURL.replace( | ||
match, | ||
serializeArrayParam(name, value, { style, explode }), | ||
); | ||
continue; | ||
} | ||
if (typeof value === "object") { | ||
nextURL = nextURL.replace( | ||
match, | ||
serializeObjectParam(name, value, { style, explode }), | ||
); | ||
continue; | ||
} | ||
if (style === "matrix") { | ||
nextURL = nextURL.replace( | ||
match, | ||
`;${serializePrimitiveParam(name, value)}`, | ||
); | ||
continue; | ||
} | ||
nextURL = nextURL.replace(match, style === "label" ? `.${value}` : value); | ||
continue; | ||
} | ||
return output; | ||
return nextURL; | ||
} | ||
@@ -289,8 +450,9 @@ | ||
let finalURL = `${options.baseUrl}${pathname}`; | ||
if (options.params.path) { | ||
for (const [k, v] of Object.entries(options.params.path)) { | ||
finalURL = finalURL.replace(`{${k}}`, encodeURIComponent(String(v))); | ||
} | ||
if (options.params?.path) { | ||
finalURL = defaultPathSerializer(finalURL, options.params.path); | ||
} | ||
const search = options.querySerializer(options.params.query ?? {}); | ||
let search = options.querySerializer(options.params.query ?? {}); | ||
if (search.startsWith("?")) { | ||
search = search.substring(1); | ||
} | ||
if (search) { | ||
@@ -297,0 +459,0 @@ finalURL += `?${search}`; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
105990
2120
+ Addedopenapi-typescript-helpers@0.0.7(transitive)
- Removedopenapi-typescript-helpers@0.0.6(transitive)