@openapi-qraft/react 2.6.3 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,31 +12,29 @@ import { QueryClient, useQueries } from '@tanstack/react-query';
12
12
  import { createElement, Fragment, useEffect, useMemo } from 'react';
13
13
  import { jwtDecode } from './lib/jwt-decode/index.js';
14
14
 
15
- interface QraftSecureRequestFnBaseProps {
16
- requestFn<TData, TError>(
17
- schema: OperationSchema,
18
- requestInfo: RequestFnInfo,
19
- options?: RequestFnOptions
20
- ): Promise<RequestFnResponse<TData, TError>>;
15
+ type RequestFn = <TData, TError>(
16
+ schema: OperationSchema,
17
+ requestInfo: RequestFnInfo,
18
+ options?: RequestFnOptions
19
+ ) => Promise<RequestFnResponse<TData, TError>>;
20
+
21
+ interface QraftSecureRequestFnBaseProps<TRequestFn extends RequestFn> {
22
+ requestFn: TRequestFn;
21
23
  securitySchemes: SecuritySchemeHandlers<string>;
22
24
  }
23
25
 
24
- type RequestFn = QraftSecureRequestFnBaseProps['requestFn'];
25
-
26
- export interface QraftSecureRequestFnProps
27
- extends QraftSecureRequestFnBaseProps {
28
- children(
29
- secureRequestFn: QraftSecureRequestFnBaseProps['requestFn']
30
- ): ReactNode;
26
+ export interface QraftSecureRequestFnProps<TRequestFn extends RequestFn>
27
+ extends QraftSecureRequestFnBaseProps<TRequestFn> {
28
+ children(secureRequestFn: TRequestFn): ReactNode;
31
29
  queryClient?: QueryClient;
32
30
  }
33
31
 
34
- export function QraftSecureRequestFn({
32
+ export function QraftSecureRequestFn<TRequestFn extends RequestFn>({
35
33
  children,
36
34
  requestFn,
37
35
  securitySchemes,
38
36
  queryClient: inQueryClient,
39
- }: QraftSecureRequestFnProps) {
37
+ }: QraftSecureRequestFnProps<TRequestFn>) {
40
38
  const queryClient = useMemo(
41
39
  () =>
42
40
  inQueryClient ??
@@ -72,13 +70,13 @@ export function QraftSecureRequestFn({
72
70
  });
73
71
  }
74
72
 
75
- export const useSecuritySchemeAuth = ({
73
+ export const useSecuritySchemeAuth = <TRequestFn extends RequestFn>({
76
74
  securitySchemes,
77
75
  requestFn,
78
76
  queryClient,
79
- }: QraftSecureRequestFnBaseProps & {
77
+ }: QraftSecureRequestFnBaseProps<TRequestFn> & {
80
78
  queryClient: QueryClient;
81
- }): RequestFn => {
79
+ }): TRequestFn => {
82
80
  useQueries(
83
81
  {
84
82
  queries: Object.entries(securitySchemes).map(([securitySchemeName]) => {
@@ -101,7 +99,7 @@ export const useSecuritySchemeAuth = ({
101
99
  return useMemo(
102
100
  () => createSecureRequestFn(securitySchemes, requestFn, queryClient),
103
101
  [securitySchemes, requestFn, queryClient]
104
- );
102
+ ) as TRequestFn;
105
103
  };
106
104
 
107
105
  /**
@@ -113,11 +111,11 @@ export const useSecuritySchemeAuth = ({
113
111
  * @param requestFn Qraft `requestFn`
114
112
  * @param queryClient QueryClient instance.
115
113
  */
116
- export function createSecureRequestFn(
114
+ export function createSecureRequestFn<TRequestFn extends RequestFn>(
117
115
  securitySchemes: SecuritySchemeHandlers<string>,
118
- requestFn: RequestFn,
116
+ requestFn: TRequestFn,
119
117
  queryClient: QueryClient
120
- ): RequestFn {
118
+ ): TRequestFn {
121
119
  return async function secureRequestFn(
122
120
  schema: OperationSchema,
123
121
  requestInfo: RequestFnInfo,
@@ -147,7 +145,7 @@ export function createSecureRequestFn(
147
145
  ),
148
146
  options
149
147
  );
150
- };
148
+ } as TRequestFn;
151
149
  }
152
150
 
153
151
  /**
@@ -174,14 +172,25 @@ async function createSecureRequestInfo(
174
172
  ): Promise<RequestFnInfo> {
175
173
  const prevSecurityResult = queryClient.getQueryData<SecurityScheme>(queryKey);
176
174
 
177
- const securityResult = await queryClient.fetchQuery({
178
- queryKey,
179
- queryFn: ({ signal }) =>
180
- handler({
181
- signal,
182
- isRefreshing: Boolean(prevSecurityResult),
183
- }),
184
- });
175
+ const abortQuery = () => queryClient.cancelQueries({ queryKey, exact: true });
176
+
177
+ requestInfo.signal?.addEventListener('abort', abortQuery);
178
+
179
+ const securityResult = await queryClient
180
+ .fetchQuery({
181
+ queryKey,
182
+ queryFn: ({ signal }) =>
183
+ handler({
184
+ signal,
185
+ isRefreshing: Boolean(prevSecurityResult),
186
+ }),
187
+ })
188
+ .catch((error) => {
189
+ throw requestInfo.signal?.aborted ? requestInfo.signal.reason : error;
190
+ })
191
+ .finally(
192
+ () => void requestInfo.signal?.removeEventListener('abort', abortQuery)
193
+ );
185
194
 
186
195
  if (!shallowEqualObjects(securityResult, prevSecurityResult)) {
187
196
  const securityRefreshInterval = getSecurityRefreshInterval(securityResult);
@@ -59,7 +59,6 @@ export const useMutation: <
59
59
  {
60
60
  ...options,
61
61
  mutationKey,
62
- // @ts-expect-error - Too complex to type
63
62
  mutationFn:
64
63
  options?.mutationFn ??
65
64
  (parameters
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveResponse } from '../responseUtils.js';
3
+
4
+ describe('resolveResponse', () => {
5
+ it('should handle Error without promise', async () => {
6
+ const testError = new Error('Test error');
7
+
8
+ const result = await resolveResponse<unknown, Error>(testError);
9
+
10
+ expect(result).toEqual({
11
+ error: testError,
12
+ response: undefined,
13
+ data: undefined,
14
+ });
15
+ });
16
+
17
+ it('should handle Response without promise', async () => {
18
+ const mockResponse = new Response(null);
19
+
20
+ // We use casting to bypass type checking, since we know that the function accepts Response
21
+ // without a promise in the implementation, although types do not allow it directly.
22
+ const result = await (resolveResponse as any)(mockResponse);
23
+
24
+ expect(result).toEqual({
25
+ error: expect.any(Error),
26
+ response: mockResponse,
27
+ data: undefined,
28
+ });
29
+ expect((result.error as Error).message).toBe(
30
+ 'Unhandled response without promise to resolve'
31
+ );
32
+ });
33
+
34
+ it('should resolve promise with data and response', async () => {
35
+ const mockResponse = new Response(null);
36
+ const testData = { test: 'data' };
37
+ const dataPromise = Promise.resolve(testData);
38
+
39
+ const result = await resolveResponse<typeof testData, Error>(
40
+ mockResponse,
41
+ dataPromise
42
+ );
43
+
44
+ expect(result).toEqual({
45
+ data: testData,
46
+ response: mockResponse,
47
+ error: undefined,
48
+ });
49
+ });
50
+
51
+ it('should handle rejected promise with error and response', async () => {
52
+ const mockResponse = new Response(null);
53
+ const testError = new Error('Test rejection');
54
+ const dataPromise = Promise.reject(testError);
55
+
56
+ const result = await resolveResponse<unknown, Error>(
57
+ mockResponse,
58
+ dataPromise
59
+ );
60
+
61
+ expect(result).toEqual({
62
+ error: testError,
63
+ response: mockResponse,
64
+ data: undefined,
65
+ });
66
+ });
67
+
68
+ it('should handle custom error objects in rejected promise', async () => {
69
+ const mockResponse = new Response(null);
70
+ type CustomError = { code: string; message: string };
71
+ const customError: CustomError = {
72
+ code: 'CUSTOM_ERROR',
73
+ message: 'Custom error object',
74
+ };
75
+ const dataPromise = Promise.reject(customError);
76
+
77
+ const result = await resolveResponse<unknown, CustomError>(
78
+ mockResponse,
79
+ dataPromise
80
+ );
81
+
82
+ expect(result).toEqual({
83
+ error: customError,
84
+ response: mockResponse,
85
+ data: undefined,
86
+ });
87
+ });
88
+
89
+ it('should preserve response object when promise resolves', async () => {
90
+ const headers = new Headers();
91
+ headers.append('Content-Type', 'application/json');
92
+ const mockResponse = new Response('{"key":"value"}', { headers });
93
+ const testData = { key: 'value' };
94
+ const dataPromise = Promise.resolve(testData);
95
+
96
+ const result = await resolveResponse<typeof testData, Error>(
97
+ mockResponse,
98
+ dataPromise
99
+ );
100
+
101
+ expect(result.response).toBe(mockResponse);
102
+ expect(result.data).toEqual(testData);
103
+ expect(result.error).toBeUndefined();
104
+ });
105
+ });
@@ -49,12 +49,21 @@ export function resolveResponse<TData, TError>(
49
49
  responseToReturn: Response | Error,
50
50
  responsePromise?: Promise<TData>
51
51
  ): Promise<RequestFnResponse<TData, TError>> {
52
- if (!responsePromise)
53
- return Promise.reject({
54
- error: responseToReturn,
55
- response: undefined,
56
- data: undefined,
57
- });
52
+ if (!responsePromise) {
53
+ if (responseToReturn instanceof Response) {
54
+ return Promise.resolve({
55
+ error: new Error('Unhandled response without promise to resolve'),
56
+ response: responseToReturn,
57
+ data: undefined,
58
+ });
59
+ } else {
60
+ return Promise.resolve({
61
+ error: responseToReturn,
62
+ response: undefined,
63
+ data: undefined,
64
+ });
65
+ }
66
+ }
58
67
 
59
68
  return responsePromise
60
69
  .then(
@@ -10,10 +10,10 @@ import type {
10
10
  import { createRecursiveProxy } from './lib/createRecursiveProxy.js';
11
11
 
12
12
  export interface CreateAPIBasicClientOptions {
13
- requestFn<TData, TError>(
13
+ requestFn: (
14
14
  schema: OperationSchema,
15
15
  requestInfo: RequestFnInfo
16
- ): Promise<RequestFnResponse<TData, TError>>;
16
+ ) => Promise<RequestFnResponse<any, any>>;
17
17
  baseUrl: string;
18
18
  }
19
19