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.

201 lines (197 loc) 8.19 kB
'use client'; 'use strict'; var reactQuery = require('@tanstack/react-query'); var react = require('react'); var index = require('./lib/jwt-decode/index.cjs'); function QraftSecureRequestFn({ children, requestFn, securitySchemes, queryClient: inQueryClient }) { const queryClient = react.useMemo(()=>inQueryClient ?? new reactQuery.QueryClient({ defaultOptions: { queries: { staleTime: 300_000, gcTime: 300_000, refetchOnWindowFocus: false, refetchOnMount: false, refetchOnReconnect: false } } }), [ inQueryClient ]); react.useEffect(()=>{ queryClient.mount(); return ()=>{ return void queryClient.unmount(); }; }, [ queryClient ]); const secureRequestFn = useSecuritySchemeAuth({ securitySchemes, requestFn, queryClient }); return react.createElement(react.Fragment, { children: children(secureRequestFn) }); } const useSecuritySchemeAuth = ({ securitySchemes, requestFn, queryClient })=>{ reactQuery.useQueries({ queries: Object.entries(securitySchemes).map(([securitySchemeName])=>{ const queryKey = [ securitySchemeName ]; return { enabled: Boolean(queryClient.getQueryData(queryKey)), queryKey: queryKey, queryFn: ({ signal })=>{ const securitySchemeHandler = securitySchemes[securitySchemeName]; return securitySchemeHandler({ signal, isRefreshing: true }); } }; }) }, queryClient); return react.useMemo(()=>createSecureRequestFn(securitySchemes, requestFn, queryClient), [ securitySchemes, requestFn, queryClient ]); }; /** * Create a secure `requestFn` that manages security schemes. * It will automatically fetch and refresh tokens when needed * using QueryClient and the provided security schemes. * * @param securitySchemes OpenAPI security schemes. * @param requestFn Qraft `requestFn` * @param queryClient QueryClient instance. */ function createSecureRequestFn(securitySchemes, requestFn, queryClient) { return async function secureRequestFn(schema, requestInfo, options) { const availableSecuritySchemes = Object.keys(securitySchemes); const securitySchemeName = schema.security?.find((security)=>availableSecuritySchemes.includes(security)); if (!securitySchemeName) { return requestFn(schema, requestInfo, options); } const securitySchemeHandler = securitySchemes[securitySchemeName]; return requestFn(schema, await createSecureRequestInfo(securitySchemeHandler, [ securitySchemeName ], requestInfo, queryClient), options); }; } /** * Calculate the interval at which a token should be refreshed. */ function getJwtTokenRefreshInterval(token) { const parsedToken = index.jwtDecode(token); if (!parsedToken.iat || !parsedToken.exp) { throw new Error("JWT token must contain both 'iat' and 'exp' fields."); } return (parsedToken.exp - parsedToken.iat) * 1000; } /** * Refresh the token if it is about to expire * @returns The string to be used as the `Authorization` header or the object to be used in the request. */ async function createSecureRequestInfo(handler, queryKey, requestInfo, queryClient) { const prevSecurityResult = queryClient.getQueryData(queryKey); const abortQuery = ()=>queryClient.cancelQueries({ queryKey, exact: true }); requestInfo.signal?.addEventListener('abort', abortQuery); const securityResult = await queryClient.fetchQuery({ queryKey, queryFn: ({ signal })=>handler({ signal, isRefreshing: Boolean(prevSecurityResult) }) }).catch((error)=>{ throw requestInfo.signal?.aborted ? requestInfo.signal.reason : error; }).finally(()=>void requestInfo.signal?.removeEventListener('abort', abortQuery)); if (!shallowEqualObjects(securityResult, prevSecurityResult)) { const securityRefreshInterval = getSecurityRefreshInterval(securityResult); if (securityRefreshInterval !== undefined && securityRefreshInterval > 0) { // remove the already fetched query, before setting new defaults queryClient.removeQueries({ queryKey, exact: true }); // set query new defaults (staleTime and gcTime) setSecurityQueryDefaults(queryClient, queryKey, securityRefreshInterval); // Set query data with the new token, since the old one was removed. // New query data will follow the new defaults staleTime and gcTime. queryClient.setQueryData(queryKey, securityResult); } } const securityParameters = createSecureRequestParameters(securityResult); if (!securityParameters) return requestInfo; return { ...requestInfo, parameters: { ...requestInfo.parameters, [securityParameters.in]: { [securityParameters.name]: securityParameters.value, ...requestInfo.parameters?.[securityParameters.in] } } }; } function setSecurityQueryDefaults(queryClient, queryKey, refreshInterval) { // set staleTime and gcTime based on token lifetime queryClient.setQueryDefaults(queryKey, { // stale the token 90% before it expires staleTime: isFinite(refreshInterval) ? refreshInterval * 0.9 : refreshInterval, // garbage collect the token after it expires gcTime: isFinite(refreshInterval) ? refreshInterval : refreshInterval, // refetch the token 80% before it expires refetchInterval: isFinite(refreshInterval) ? refreshInterval * 0.8 : refreshInterval, // always refetch the token, even in the background refetchIntervalInBackground: true }); } function createSecureRequestParameters(securityResult) { if (typeof securityResult === 'string' || 'token' in securityResult) { return { in: 'header', name: 'Authorization', value: `Bearer ${typeof securityResult === 'string' ? securityResult : securityResult.token}` }; } if ('credentials' in securityResult) { return { in: 'header', name: 'Authorization', value: `Basic ${securityResult.credentials}` }; } if ('in' in securityResult && securityResult.in !== 'cookie') { return securityResult; } throw new Error('Security scheme must be a string, an object with a token property, an object with a credentials property, or an object with an in property that is not equal to "cookie".'); } function getSecurityRefreshInterval(securityResult) { if (typeof securityResult === 'string') { return getJwtTokenRefreshInterval(securityResult); } if ('refreshInterval' in securityResult) { return securityResult.refreshInterval; } } /** * @internal */ function shallowEqualObjects(newObj, prevObj) { if (typeof newObj !== typeof prevObj) return false; if (typeof newObj === 'string') return newObj === prevObj; if (typeof newObj !== 'object' || newObj === null || typeof prevObj !== 'object' || prevObj === null || prevObj === undefined) return false; if (Object.keys(newObj).length !== Object.keys(prevObj).length) return false; for(const key in newObj){ if (!Object.prototype.hasOwnProperty.call(newObj, key)) return false; if (newObj[key] !== prevObj[key]) return false; } return true; } exports.QraftSecureRequestFn = QraftSecureRequestFn; exports.createSecureRequestFn = createSecureRequestFn; exports.shallowEqualObjects = shallowEqualObjects; exports.useSecuritySchemeAuth = useSecuritySchemeAuth; //# sourceMappingURL=Unstable_QraftSecureRequestFn.cjs.map