UNPKG

@openapi-qraft/react

Version:

OpenAPI client for React, providing type-safe requests and dynamic TanStack Query React Hooks via a modular, Proxy-based architecture.

195 lines (192 loc) 8.55 kB
import { processResponse, resolveResponse } from './responseUtils.js'; /** * 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(processResponse).catch(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'; } export { baseRequestFn, bodySerializer, mergeHeaders, requestFn, urlSerializer }; //# sourceMappingURL=requestFn.js.map