@openapi-qraft/react
Version:
OpenAPI client for React, providing type-safe requests and dynamic TanStack Query React Hooks via a modular, Proxy-based architecture.
201 lines (197 loc) • 8.68 kB
JavaScript
;
var responseUtils = require('./responseUtils.cjs');
/**
* This function is used to make a request to a specified endpoint.
*
* @template T The expected return type of the request.
*
* @param schema The schema of the operation to be performed. It includes the OpenAPI path, HTTP method and media type.
* @param requestInfo The information required to make the request. It includes parameters, headers, body, etc.
* @param [options] Optional. Additional options for the request. It includes custom urlSerializer, bodySerializer, and fetch function.
*
* @returns {Promise<T>} Returns a promise that resolves with the response of the request.
*
* @throws {error: object|string, response: Response} Throws an error if the request fails. The error includes the error message and the response from the server.
*/ async function requestFn(schema, requestInfo, options) {
return baseRequestFn(schema, requestInfo, {
urlSerializer,
bodySerializer,
...options
});
}
/**
* This function is used to make a request to a specified endpoint.
* It's necessary to create a custom `requestFn` with a custom `urlSerializer`
* and `bodySerializer`, with the tree-shaking of the default `requestFn`
* and its serializers.
*
* @template T The expected return type of the request.
*
* @param requestSchema The schema of the operation to be performed. It includes the OpenAPI path, HTTP method and media type.
* @param requestInfo The information required to make the request. It includes parameters, headers, body, etc.
* @param options The options for the request. It includes urlSerializer, bodySerializer, and fetch function. The 'urlSerializer' and 'bodySerializer' are required.
*
* @returns {Promise<T>} Returns a promise that resolves with the response of the request.
*
* @throws {error: object|string, response: Response} Throws an error if the request fails. The error includes the error message and the response from the server.
*/ async function baseRequestFn(requestSchema, requestInfo, options) {
const { parameters, headers, body, ...requestInfoRest } = requestInfo;
const requestPayload = options.bodySerializer(requestSchema, body);
const baseFetch = options.fetch ?? fetch;
return baseFetch(options.urlSerializer(requestSchema, requestInfo), {
method: requestSchema.method.toUpperCase(),
body: requestPayload?.body,
headers: mergeHeaders({
Accept: 'application/json'
}, requestPayload?.headers, headers, parameters?.header),
...requestInfoRest
}).then(responseUtils.processResponse).catch(responseUtils.resolveResponse);
}
/**
* Serializes the given schema and request payload into a URL string.
*
* This function is implemented according to the URI Template standard
* defined in RFC 6570. It supports the expansion of path and query parameters,
* correctly handling empty arrays, `null`, and `undefined` values by ignoring
* them during the expansion process, as specified by the standard.
*
* For more information, refer to the official documentation:
* https://datatracker.ietf.org/doc/html/rfc6570
*
* @param schema - The operation schema containing the URL template and method.
* @param info - The request payload including baseUrl, path parameters, and query parameters.
* @returns The fully constructed URL string.
*/ function urlSerializer(schema, info) {
const path = schema.url.replace(/{(.*?)}/g, (substring, group)=>{
if (info.parameters?.path && Object.prototype.hasOwnProperty.call(info.parameters.path, group)) {
return encodeURIComponent(String(info.parameters?.path[group]));
}
return substring;
});
const baseUrl = info.baseUrl ?? '';
if (info.parameters?.query) {
return `${baseUrl}${path}${getQueryString(info.parameters.query)}`;
}
return `${baseUrl}${path}`;
}
function getQueryString(params) {
const search = new URLSearchParams();
const walk = (prefix, value)=>{
if (value == null) return;
if (value instanceof Date) return search.append(prefix, value.toISOString());
if (Array.isArray(value)) return value.forEach((v)=>walk(prefix, v));
if (typeof value === 'object') {
return Object.entries(value).forEach(([k, v])=>walk(`${prefix}[${k}]`, v));
}
search.append(prefix, String(value));
};
Object.entries(params).forEach(([k, v])=>walk(k, v));
const searchString = search.toString();
return searchString ? `?${searchString}` : '';
}
function mergeHeaders(...allHeaders) {
const headers = new Headers();
for (const headerSet of allHeaders){
if (!headerSet || typeof headerSet !== 'object') {
continue;
}
const headersIterator = headerSet instanceof Headers ? headerSet.entries() : Object.entries(headerSet);
for (const [headerKey, headerValue] of headersIterator){
if (headerValue === null) {
headers.delete(headerKey);
} else if (headerValue !== undefined) {
headers.set(headerKey, headerValue);
}
}
}
return headers;
}
function bodySerializer(schema, body) {
if (isReadOnlyOperation(schema)) return undefined;
if (body === undefined || body === null) return undefined;
if (typeof body === 'string') {
return {
body,
headers: {
'Content-Type': // prefer text/* media type
schema.mediaType?.find((mediaType)=>mediaType.includes('text/')) ?? // prefer JSON media type, assume that body is serialized as JSON
schema.mediaType?.find((mediaType)=>mediaType.includes('/json')) ?? 'text/plain'
}
};
}
if (body instanceof FormData) {
return {
body,
headers: {
// remove `Content-Type` if the serialized body is FormData;
// the browser will correctly set Content-Type & boundary expression
'Content-Type': null
}
};
}
if (body instanceof Blob) {
return {
body,
headers: {
'Content-Type': body.type || schema.mediaType?.find((mediaType)=>!(mediaType.includes('text/') && mediaType.includes('/form-data') && mediaType.includes('/json'))) || 'application/octet-stream'
}
};
}
let jsonMediaType = null;
let formDataMediaType = null;
if (schema.mediaType) {
for(let i = 0; i < schema.mediaType.length; i++){
const mediaType = schema.mediaType[i];
if (mediaType.includes('/json')) jsonMediaType = mediaType;
else if (mediaType.includes('/form-data')) formDataMediaType = mediaType;
}
}
if (formDataMediaType) {
if (!jsonMediaType || // Prefer FormData serialization if one of the fields is a Blob
Object.values(body).some((value)=>value instanceof Blob)) {
return {
body: getRequestBodyFormData(body),
headers: {
// remove `Content-Type` if the serialized body is FormData;
// the browser will correctly set Content-Type & boundary expression
'Content-Type': null
}
};
}
}
return {
body: JSON.stringify(body),
headers: {
'Content-Type': jsonMediaType ?? 'application/json'
}
};
}
function getRequestBodyFormData(body) {
if (body instanceof FormData) return body;
if (typeof body !== 'object') throw new Error(`Unsupported body type ${typeof body} in form-data.`);
const formData = new FormData();
const process = (key, value)=>{
if (typeof value === 'string' || value instanceof Blob) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
Object.entries(body).filter(([_, value])=>typeof value !== 'undefined' && value !== null).forEach(([key, value])=>{
if (Array.isArray(value)) {
value.forEach((v)=>process(key, v));
} else {
process(key, value);
}
});
return formData;
}
function isReadOnlyOperation(operation) {
return operation.method === 'get' || operation.method === 'head' || operation.method === 'trace' || operation.method === 'options';
}
exports.baseRequestFn = baseRequestFn;
exports.bodySerializer = bodySerializer;
exports.mergeHeaders = mergeHeaders;
exports.requestFn = requestFn;
exports.urlSerializer = urlSerializer;
//# sourceMappingURL=requestFn.cjs.map