openapi-fetch
Version:
Fast, type-safe fetch client for your OpenAPI schema. Only 6 kb (min). Works with React, Vue, Svelte, or vanilla JS.
548 lines (544 loc) • 17.7 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
const PATH_PARAM_RE = /\{[^{}]+\}/g;
const supportsRequestInitExt = () => {
return typeof process === "object" && Number.parseInt(process?.versions?.node?.substring(0, 2)) >= 18 && process.versions.undici;
};
function randomID() {
return Math.random().toString(36).slice(2, 11);
}
function createClient(clientOptions) {
let {
baseUrl = "",
Request: CustomRequest = globalThis.Request,
fetch: baseFetch = globalThis.fetch,
querySerializer: globalQuerySerializer,
bodySerializer: globalBodySerializer,
pathSerializer: globalPathSerializer,
headers: baseHeaders,
requestInitExt = void 0,
...baseOptions
} = { ...clientOptions };
requestInitExt = supportsRequestInitExt() ? requestInitExt : void 0;
baseUrl = removeTrailingSlash(baseUrl);
const globalMiddlewares = [];
async function coreFetch(schemaPath, fetchOptions) {
const {
baseUrl: localBaseUrl,
fetch = baseFetch,
Request = CustomRequest,
headers,
params = {},
parseAs = "json",
querySerializer: requestQuerySerializer,
bodySerializer = globalBodySerializer ?? defaultBodySerializer,
pathSerializer: requestPathSerializer,
body,
middleware: requestMiddlewares = [],
...init
} = fetchOptions || {};
let finalBaseUrl = baseUrl;
if (localBaseUrl) {
finalBaseUrl = removeTrailingSlash(localBaseUrl) ?? baseUrl;
}
let querySerializer = typeof globalQuerySerializer === "function" ? globalQuerySerializer : createQuerySerializer(globalQuerySerializer);
if (requestQuerySerializer) {
querySerializer = typeof requestQuerySerializer === "function" ? requestQuerySerializer : createQuerySerializer({
...typeof globalQuerySerializer === "object" ? globalQuerySerializer : {},
...requestQuerySerializer
});
}
const pathSerializer = requestPathSerializer || globalPathSerializer || defaultPathSerializer;
const serializedBody = body === void 0 ? void 0 : bodySerializer(
body,
// Note: we declare mergeHeaders() both here and below because it’s a bit of a chicken-or-egg situation:
// bodySerializer() needs all headers so we aren’t dropping ones set by the user, however,
// the result of this ALSO sets the lowest-priority content-type header. So we re-merge below,
// setting the content-type at the very beginning to be overwritten.
// Lastly, based on the way headers work, it’s not a simple “present-or-not” check becauase null intentionally un-sets headers.
mergeHeaders(baseHeaders, headers, params.header)
);
const finalHeaders = mergeHeaders(
// with no body, we should not to set Content-Type
serializedBody === void 0 || // if serialized body is FormData; browser will correctly set Content-Type & boundary expression
serializedBody instanceof FormData ? {} : {
"Content-Type": "application/json"
},
baseHeaders,
headers,
params.header
);
const finalMiddlewares = [...globalMiddlewares, ...requestMiddlewares];
const requestInit = {
redirect: "follow",
...baseOptions,
...init,
body: serializedBody,
headers: finalHeaders
};
let id;
let options;
let request = new Request(
createFinalURL(schemaPath, { baseUrl: finalBaseUrl, params, querySerializer, pathSerializer }),
requestInit
);
let response;
for (const key in init) {
if (!(key in request)) {
request[key] = init[key];
}
}
if (finalMiddlewares.length) {
id = randomID();
options = Object.freeze({
baseUrl: finalBaseUrl,
fetch,
parseAs,
querySerializer,
bodySerializer,
pathSerializer
});
for (const m of finalMiddlewares) {
if (m && typeof m === "object" && typeof m.onRequest === "function") {
const result = await m.onRequest({
request,
schemaPath,
params,
options,
id
});
if (result) {
if (result instanceof Request) {
request = result;
} else if (result instanceof Response) {
response = result;
break;
} else {
throw new Error("onRequest: must return new Request() or Response() when modifying the request");
}
}
}
}
}
if (!response) {
try {
response = await fetch(request, requestInitExt);
} catch (error2) {
let errorAfterMiddleware = error2;
if (finalMiddlewares.length) {
for (let i = finalMiddlewares.length - 1; i >= 0; i--) {
const m = finalMiddlewares[i];
if (m && typeof m === "object" && typeof m.onError === "function") {
const result = await m.onError({
request,
error: errorAfterMiddleware,
schemaPath,
params,
options,
id
});
if (result) {
if (result instanceof Response) {
errorAfterMiddleware = void 0;
response = result;
break;
}
if (result instanceof Error) {
errorAfterMiddleware = result;
continue;
}
throw new Error("onError: must return new Response() or instance of Error");
}
}
}
}
if (errorAfterMiddleware) {
throw errorAfterMiddleware;
}
}
if (finalMiddlewares.length) {
for (let i = finalMiddlewares.length - 1; i >= 0; i--) {
const m = finalMiddlewares[i];
if (m && typeof m === "object" && typeof m.onResponse === "function") {
const result = await m.onResponse({
request,
response,
schemaPath,
params,
options,
id
});
if (result) {
if (!(result instanceof Response)) {
throw new Error("onResponse: must return new Response() when modifying the response");
}
response = result;
}
}
}
}
}
const contentLength = response.headers.get("Content-Length");
if (response.status === 204 || request.method === "HEAD" || contentLength === "0" && !response.headers.get("Transfer-Encoding")?.includes("chunked")) {
return response.ok ? { data: void 0, response } : { error: void 0, response };
}
if (response.ok) {
const getResponseData = async () => {
if (parseAs === "stream") {
return response.body;
}
if (parseAs === "json" && !contentLength) {
const raw = await response.text();
return raw ? JSON.parse(raw) : void 0;
}
return await response[parseAs]();
};
return { data: await getResponseData(), response };
}
let error = await response.text();
try {
error = JSON.parse(error);
} catch {
}
return { error, response };
}
return {
request(method, url, init) {
return coreFetch(url, { ...init, method: method.toUpperCase() });
},
/** Call a GET endpoint */
GET(url, init) {
return coreFetch(url, { ...init, method: "GET" });
},
/** Call a PUT endpoint */
PUT(url, init) {
return coreFetch(url, { ...init, method: "PUT" });
},
/** Call a POST endpoint */
POST(url, init) {
return coreFetch(url, { ...init, method: "POST" });
},
/** Call a DELETE endpoint */
DELETE(url, init) {
return coreFetch(url, { ...init, method: "DELETE" });
},
/** Call a OPTIONS endpoint */
OPTIONS(url, init) {
return coreFetch(url, { ...init, method: "OPTIONS" });
},
/** Call a HEAD endpoint */
HEAD(url, init) {
return coreFetch(url, { ...init, method: "HEAD" });
},
/** Call a PATCH endpoint */
PATCH(url, init) {
return coreFetch(url, { ...init, method: "PATCH" });
},
/** Call a TRACE endpoint */
TRACE(url, init) {
return coreFetch(url, { ...init, method: "TRACE" });
},
/** Register middleware */
use(...middleware) {
for (const m of middleware) {
if (!m) {
continue;
}
if (typeof m !== "object" || !("onRequest" in m || "onResponse" in m || "onError" in m)) {
throw new Error("Middleware must be an object with one of `onRequest()`, `onResponse() or `onError()`");
}
globalMiddlewares.push(m);
}
},
/** Unregister middleware */
eject(...middleware) {
for (const m of middleware) {
const i = globalMiddlewares.indexOf(m);
if (i !== -1) {
globalMiddlewares.splice(i, 1);
}
}
}
};
}
class PathCallForwarder {
constructor(client, url) {
this.client = client;
this.url = url;
}
GET = (init) => {
return this.client.GET(this.url, init);
};
PUT = (init) => {
return this.client.PUT(this.url, init);
};
POST = (init) => {
return this.client.POST(this.url, init);
};
DELETE = (init) => {
return this.client.DELETE(this.url, init);
};
OPTIONS = (init) => {
return this.client.OPTIONS(this.url, init);
};
HEAD = (init) => {
return this.client.HEAD(this.url, init);
};
PATCH = (init) => {
return this.client.PATCH(this.url, init);
};
TRACE = (init) => {
return this.client.TRACE(this.url, init);
};
}
class PathClientProxyHandler {
constructor() {
this.client = null;
}
// Assume the property is an URL.
get(coreClient, url) {
const forwarder = new PathCallForwarder(coreClient, url);
this.client[url] = forwarder;
return forwarder;
}
}
function wrapAsPathBasedClient(coreClient) {
const handler = new PathClientProxyHandler();
const proxy = new Proxy(coreClient, handler);
function Client() {
}
Client.prototype = proxy;
const client = new Client();
handler.client = client;
return client;
}
function createPathBasedClient(clientOptions) {
return wrapAsPathBasedClient(createClient(clientOptions));
}
function serializePrimitiveParam(name, value, options) {
if (value === void 0 || value === null) {
return "";
}
if (typeof value === "object") {
throw new Error(
"Deeply-nested arrays/objects aren\u2019t supported. Provide your own `querySerializer()` to handle these."
);
}
return `${name}=${options?.allowReserved === true ? value : encodeURIComponent(value)}`;
}
function serializeObjectParam(name, value, options) {
if (!value || typeof value !== "object") {
return "";
}
const values = [];
const joiner = {
simple: ",",
label: ".",
matrix: ";"
}[options.style] || "&";
if (options.style !== "deepObject" && options.explode === false) {
for (const k in value) {
values.push(k, options.allowReserved === true ? value[k] : encodeURIComponent(value[k]));
}
const final2 = values.join(",");
switch (options.style) {
case "form": {
return `${name}=${final2}`;
}
case "label": {
return `.${final2}`;
}
case "matrix": {
return `;${name}=${final2}`;
}
default: {
return final2;
}
}
}
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;
}
function serializeArrayParam(name, value, options) {
if (!Array.isArray(value)) {
return "";
}
if (options.explode === false) {
const joiner2 = { form: ",", spaceDelimited: "%20", pipeDelimited: "|" }[options.style] || ",";
const final = (options.allowReserved === true ? value : value.map((v) => encodeURIComponent(v))).join(joiner2);
switch (options.style) {
case "simple": {
return final;
}
case "label": {
return `.${final}`;
}
case "matrix": {
return `;${name}=${final}`;
}
// case "spaceDelimited":
// case "pipeDelimited":
default: {
return `${name}=${final}`;
}
}
}
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));
}
}
return options.style === "label" || options.style === "matrix" ? `${joiner}${values.join(joiner)}` : values.join(joiner);
}
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 === void 0 || value === null) {
continue;
}
if (Array.isArray(value)) {
if (value.length === 0) {
continue;
}
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 search.join("&");
};
}
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] === void 0 || 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" ? `.${encodeURIComponent(value)}` : encodeURIComponent(value));
}
return nextURL;
}
function defaultBodySerializer(body, headers) {
if (body instanceof FormData) {
return body;
}
if (headers) {
const contentType = headers.get instanceof Function ? headers.get("Content-Type") ?? headers.get("content-type") : headers["Content-Type"] ?? headers["content-type"];
if (contentType === "application/x-www-form-urlencoded") {
return new URLSearchParams(body).toString();
}
}
return JSON.stringify(body);
}
function createFinalURL(pathname, options) {
let finalURL = `${options.baseUrl}${pathname}`;
if (options.params?.path) {
finalURL = options.pathSerializer(finalURL, options.params.path);
}
let search = options.querySerializer(options.params.query ?? {});
if (search.startsWith("?")) {
search = search.substring(1);
}
if (search) {
finalURL += `?${search}`;
}
return finalURL;
}
function mergeHeaders(...allHeaders) {
const finalHeaders = new Headers();
for (const h of allHeaders) {
if (!h || typeof h !== "object") {
continue;
}
const iterator = h instanceof Headers ? h.entries() : Object.entries(h);
for (const [k, v] of iterator) {
if (v === null) {
finalHeaders.delete(k);
} else if (Array.isArray(v)) {
for (const v2 of v) {
finalHeaders.append(k, v2);
}
} else if (v !== void 0) {
finalHeaders.set(k, v);
}
}
}
return finalHeaders;
}
function removeTrailingSlash(url) {
if (url.endsWith("/")) {
return url.substring(0, url.length - 1);
}
return url;
}
exports.createFinalURL = createFinalURL;
exports.createPathBasedClient = createPathBasedClient;
exports.createQuerySerializer = createQuerySerializer;
exports.default = createClient;
exports.defaultBodySerializer = defaultBodySerializer;
exports.defaultPathSerializer = defaultPathSerializer;
exports.mergeHeaders = mergeHeaders;
exports.randomID = randomID;
exports.removeTrailingSlash = removeTrailingSlash;
exports.serializeArrayParam = serializeArrayParam;
exports.serializeObjectParam = serializeObjectParam;
exports.serializePrimitiveParam = serializePrimitiveParam;
exports.wrapAsPathBasedClient = wrapAsPathBasedClient;
//# sourceMappingURL=index.cjs.map