@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
JavaScript
'use client';
;
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