api-core-lib 4.3.3 → 4.4.4
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.
- package/dist/index.d.mts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +141 -39
- package/dist/index.mjs +141 -39
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -191,6 +191,7 @@ interface ActionOptions {
|
|
|
191
191
|
* update('123', { status: 'active' }, { endpoint: '/items/123/activate' })
|
|
192
192
|
*/
|
|
193
193
|
endpoint?: string;
|
|
194
|
+
refetch?: boolean;
|
|
194
195
|
}
|
|
195
196
|
/**
|
|
196
197
|
* واجهة لتهيئة الهوك `useApi`.
|
|
@@ -204,6 +205,11 @@ interface UseApiConfig<T> {
|
|
|
204
205
|
refetchAfterChange?: boolean;
|
|
205
206
|
onSuccess?: (message: string, data?: T) => void;
|
|
206
207
|
onError?: (message: string, error?: ApiError) => void;
|
|
208
|
+
/**
|
|
209
|
+
* إعدادات Axios/Request افتراضية يتم تطبيقها على جميع طلبات الجلب (GET).
|
|
210
|
+
* مفيدة لتمرير إعدادات مثل isPublic.
|
|
211
|
+
*/
|
|
212
|
+
requestConfig?: RequestConfig;
|
|
207
213
|
}
|
|
208
214
|
|
|
209
215
|
/**
|
|
@@ -217,12 +223,19 @@ interface UseApiConfig<T> {
|
|
|
217
223
|
declare function createApiClient(config: ApiClientConfig): AxiosInstance;
|
|
218
224
|
|
|
219
225
|
type CrudRequestConfig = RequestConfig & ActionOptions;
|
|
226
|
+
/**
|
|
227
|
+
* دالة مصنع (Factory Function) لإنشاء مجموعة خدمات API قابلة لإعادة الاستخدام لنقطة نهاية (endpoint) محددة.
|
|
228
|
+
* توفر عمليات CRUD كاملة بالإضافة إلى ميزات متقدمة مثل الحذف الجماعي ورفع الملفات.
|
|
229
|
+
*/
|
|
220
230
|
declare function createApiServices<T>(axiosInstance: AxiosInstance, endpoint: string): {
|
|
221
231
|
get: (id?: string, config?: RequestConfig) => Promise<StandardResponse<T | T[]>>;
|
|
222
232
|
getWithQuery: (query: string, config?: RequestConfig) => Promise<StandardResponse<T[]>>;
|
|
223
233
|
post: (data: Partial<T>, config?: CrudRequestConfig) => Promise<StandardResponse<T>>;
|
|
234
|
+
put: (id: string, data: T, config?: CrudRequestConfig) => Promise<StandardResponse<T>>;
|
|
224
235
|
patch: (id: string, data: Partial<T>, config?: CrudRequestConfig) => Promise<StandardResponse<T>>;
|
|
225
236
|
remove: (id: string, config?: CrudRequestConfig) => Promise<StandardResponse<any>>;
|
|
237
|
+
bulkDelete: (ids: string[], config?: CrudRequestConfig) => Promise<StandardResponse<any>>;
|
|
238
|
+
upload: (file: File, additionalData?: Record<string, any>, config?: CrudRequestConfig) => Promise<StandardResponse<any>>;
|
|
226
239
|
};
|
|
227
240
|
|
|
228
241
|
/**
|
|
@@ -303,8 +316,11 @@ declare function useApi<T extends {
|
|
|
303
316
|
actions: {
|
|
304
317
|
fetch: (options?: QueryOptions) => Promise<void>;
|
|
305
318
|
create: (newItem: Partial<T>, options?: ActionOptions) => Promise<StandardResponse<T>>;
|
|
319
|
+
put: (id: string, item: T, options?: ActionOptions) => Promise<StandardResponse<T>>;
|
|
306
320
|
update: (id: string, updatedItem: Partial<T>, options?: ActionOptions) => Promise<StandardResponse<T>>;
|
|
307
321
|
remove: (id: string, options?: ActionOptions) => Promise<StandardResponse<any>>;
|
|
322
|
+
bulkRemove: (ids: string[], options?: ActionOptions) => Promise<StandardResponse<any>>;
|
|
323
|
+
upload: (file: File, additionalData?: Record<string, any>, options?: ActionOptions) => Promise<StandardResponse<any>>;
|
|
308
324
|
};
|
|
309
325
|
query: {
|
|
310
326
|
options: QueryOptions;
|
package/dist/index.d.ts
CHANGED
|
@@ -191,6 +191,7 @@ interface ActionOptions {
|
|
|
191
191
|
* update('123', { status: 'active' }, { endpoint: '/items/123/activate' })
|
|
192
192
|
*/
|
|
193
193
|
endpoint?: string;
|
|
194
|
+
refetch?: boolean;
|
|
194
195
|
}
|
|
195
196
|
/**
|
|
196
197
|
* واجهة لتهيئة الهوك `useApi`.
|
|
@@ -204,6 +205,11 @@ interface UseApiConfig<T> {
|
|
|
204
205
|
refetchAfterChange?: boolean;
|
|
205
206
|
onSuccess?: (message: string, data?: T) => void;
|
|
206
207
|
onError?: (message: string, error?: ApiError) => void;
|
|
208
|
+
/**
|
|
209
|
+
* إعدادات Axios/Request افتراضية يتم تطبيقها على جميع طلبات الجلب (GET).
|
|
210
|
+
* مفيدة لتمرير إعدادات مثل isPublic.
|
|
211
|
+
*/
|
|
212
|
+
requestConfig?: RequestConfig;
|
|
207
213
|
}
|
|
208
214
|
|
|
209
215
|
/**
|
|
@@ -217,12 +223,19 @@ interface UseApiConfig<T> {
|
|
|
217
223
|
declare function createApiClient(config: ApiClientConfig): AxiosInstance;
|
|
218
224
|
|
|
219
225
|
type CrudRequestConfig = RequestConfig & ActionOptions;
|
|
226
|
+
/**
|
|
227
|
+
* دالة مصنع (Factory Function) لإنشاء مجموعة خدمات API قابلة لإعادة الاستخدام لنقطة نهاية (endpoint) محددة.
|
|
228
|
+
* توفر عمليات CRUD كاملة بالإضافة إلى ميزات متقدمة مثل الحذف الجماعي ورفع الملفات.
|
|
229
|
+
*/
|
|
220
230
|
declare function createApiServices<T>(axiosInstance: AxiosInstance, endpoint: string): {
|
|
221
231
|
get: (id?: string, config?: RequestConfig) => Promise<StandardResponse<T | T[]>>;
|
|
222
232
|
getWithQuery: (query: string, config?: RequestConfig) => Promise<StandardResponse<T[]>>;
|
|
223
233
|
post: (data: Partial<T>, config?: CrudRequestConfig) => Promise<StandardResponse<T>>;
|
|
234
|
+
put: (id: string, data: T, config?: CrudRequestConfig) => Promise<StandardResponse<T>>;
|
|
224
235
|
patch: (id: string, data: Partial<T>, config?: CrudRequestConfig) => Promise<StandardResponse<T>>;
|
|
225
236
|
remove: (id: string, config?: CrudRequestConfig) => Promise<StandardResponse<any>>;
|
|
237
|
+
bulkDelete: (ids: string[], config?: CrudRequestConfig) => Promise<StandardResponse<any>>;
|
|
238
|
+
upload: (file: File, additionalData?: Record<string, any>, config?: CrudRequestConfig) => Promise<StandardResponse<any>>;
|
|
226
239
|
};
|
|
227
240
|
|
|
228
241
|
/**
|
|
@@ -303,8 +316,11 @@ declare function useApi<T extends {
|
|
|
303
316
|
actions: {
|
|
304
317
|
fetch: (options?: QueryOptions) => Promise<void>;
|
|
305
318
|
create: (newItem: Partial<T>, options?: ActionOptions) => Promise<StandardResponse<T>>;
|
|
319
|
+
put: (id: string, item: T, options?: ActionOptions) => Promise<StandardResponse<T>>;
|
|
306
320
|
update: (id: string, updatedItem: Partial<T>, options?: ActionOptions) => Promise<StandardResponse<T>>;
|
|
307
321
|
remove: (id: string, options?: ActionOptions) => Promise<StandardResponse<any>>;
|
|
322
|
+
bulkRemove: (ids: string[], options?: ActionOptions) => Promise<StandardResponse<any>>;
|
|
323
|
+
upload: (file: File, additionalData?: Record<string, any>, options?: ActionOptions) => Promise<StandardResponse<any>>;
|
|
308
324
|
};
|
|
309
325
|
query: {
|
|
310
326
|
options: QueryOptions;
|
package/dist/index.js
CHANGED
|
@@ -103,34 +103,40 @@ function createApiClient(config) {
|
|
|
103
103
|
headers: { "Content-Type": "application/json", ...headers },
|
|
104
104
|
withCredentials
|
|
105
105
|
});
|
|
106
|
-
let
|
|
106
|
+
let isRefreshing = false;
|
|
107
|
+
let failedQueue = [];
|
|
108
|
+
const processQueue = (error, token = null) => {
|
|
109
|
+
failedQueue.forEach((prom) => {
|
|
110
|
+
if (error) {
|
|
111
|
+
prom.reject(error);
|
|
112
|
+
} else {
|
|
113
|
+
prom.resolve(token);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
failedQueue = [];
|
|
117
|
+
};
|
|
107
118
|
axiosInstance.interceptors.request.use(async (req) => {
|
|
108
119
|
req.headers["X-Request-ID"] = (0, import_uuid.v4)();
|
|
109
120
|
if (req.isPublic) {
|
|
110
|
-
console.log(`[API Core] Skipping token for public request: ${req.url}`);
|
|
111
121
|
return req;
|
|
112
122
|
}
|
|
113
|
-
if (tokenRefreshPromise) {
|
|
114
|
-
await tokenRefreshPromise;
|
|
115
|
-
}
|
|
116
123
|
let tokens = await tokenManager.getTokens();
|
|
117
124
|
const now = Date.now();
|
|
118
125
|
const tokenBuffer = 60 * 1e3;
|
|
119
|
-
if (tokens.accessToken && tokens.expiresAt && tokens.expiresAt - now < tokenBuffer) {
|
|
120
|
-
if (
|
|
126
|
+
if (tokens.accessToken && tokens.expiresAt && tokens.expiresAt - now < tokenBuffer && !isRefreshing) {
|
|
127
|
+
if (config.refreshTokenConfig) {
|
|
121
128
|
console.log("[API Core] Proactive token refresh initiated.");
|
|
122
|
-
|
|
129
|
+
isRefreshing = true;
|
|
130
|
+
try {
|
|
131
|
+
const newTokens = await refreshToken(config, tokenManager);
|
|
132
|
+
if (newTokens) tokens = newTokens;
|
|
133
|
+
} finally {
|
|
134
|
+
isRefreshing = false;
|
|
135
|
+
}
|
|
123
136
|
}
|
|
124
|
-
const newTokens = await tokenRefreshPromise;
|
|
125
|
-
tokenRefreshPromise = null;
|
|
126
|
-
if (newTokens) tokens = newTokens;
|
|
127
137
|
}
|
|
128
138
|
if (tokens.accessToken && !tokenManager.isHttpOnly()) {
|
|
129
|
-
|
|
130
|
-
req.headers.Authorization = `${tokenType} ${tokens.accessToken}`;
|
|
131
|
-
console.log(`[API Core] Token attached to request: ${req.url}`);
|
|
132
|
-
} else {
|
|
133
|
-
console.warn(`[API Core] No token attached for request: ${req.url}`);
|
|
139
|
+
req.headers.Authorization = `${tokens.tokenType || "Bearer"} ${tokens.accessToken}`;
|
|
134
140
|
}
|
|
135
141
|
return req;
|
|
136
142
|
}, (error) => Promise.reject(error));
|
|
@@ -138,19 +144,34 @@ function createApiClient(config) {
|
|
|
138
144
|
(response) => response,
|
|
139
145
|
async (error) => {
|
|
140
146
|
const originalRequest = error.config;
|
|
141
|
-
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
if (error.response?.status === 401 && !originalRequest._retry && config.refreshTokenConfig) {
|
|
148
|
+
if (isRefreshing) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
failedQueue.push({ resolve, reject });
|
|
151
|
+
}).then((token) => {
|
|
152
|
+
if (token && !tokenManager.isHttpOnly()) {
|
|
153
|
+
originalRequest.headers["Authorization"] = `Bearer ${token}`;
|
|
154
|
+
}
|
|
155
|
+
return axiosInstance(originalRequest);
|
|
156
|
+
});
|
|
146
157
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
originalRequest._retry = true;
|
|
159
|
+
isRefreshing = true;
|
|
160
|
+
try {
|
|
161
|
+
const newTokens = await refreshToken(config, tokenManager);
|
|
162
|
+
if (!newTokens || !newTokens.accessToken) {
|
|
163
|
+
throw new Error("Token refresh failed to produce a new access token.");
|
|
164
|
+
}
|
|
165
|
+
processQueue(null, newTokens.accessToken);
|
|
166
|
+
if (!tokenManager.isHttpOnly()) {
|
|
167
|
+
originalRequest.headers["Authorization"] = `${newTokens.tokenType || "Bearer"} ${newTokens.accessToken}`;
|
|
152
168
|
}
|
|
153
169
|
return axiosInstance(originalRequest);
|
|
170
|
+
} catch (refreshError) {
|
|
171
|
+
processQueue(refreshError, null);
|
|
172
|
+
return Promise.reject(refreshError);
|
|
173
|
+
} finally {
|
|
174
|
+
isRefreshing = false;
|
|
154
175
|
}
|
|
155
176
|
}
|
|
156
177
|
const enhancedError = {
|
|
@@ -267,6 +288,15 @@ function createApiServices(axiosInstance, endpoint) {
|
|
|
267
288
|
return processResponse(error);
|
|
268
289
|
}
|
|
269
290
|
};
|
|
291
|
+
const put = async (id, data, config) => {
|
|
292
|
+
const finalUrl = config?.endpoint || `${endpoint}/${id}`;
|
|
293
|
+
try {
|
|
294
|
+
const response = await axiosInstance.put(finalUrl, data, config);
|
|
295
|
+
return processResponse(response);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return processResponse(error);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
270
300
|
const patch = async (id, data, config) => {
|
|
271
301
|
const finalUrl = config?.endpoint || `${endpoint}/${id}`;
|
|
272
302
|
try {
|
|
@@ -285,7 +315,38 @@ function createApiServices(axiosInstance, endpoint) {
|
|
|
285
315
|
return processResponse(error);
|
|
286
316
|
}
|
|
287
317
|
};
|
|
288
|
-
|
|
318
|
+
const bulkDelete = async (ids, config) => {
|
|
319
|
+
const finalUrl = config?.endpoint || `${endpoint}`;
|
|
320
|
+
try {
|
|
321
|
+
const response = await axiosInstance.delete(finalUrl, { data: { ids }, ...config });
|
|
322
|
+
return processResponse(response);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return processResponse(error);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
const upload = async (file, additionalData, config) => {
|
|
328
|
+
const finalUrl = config?.endpoint || `${endpoint}`;
|
|
329
|
+
const formData = new FormData();
|
|
330
|
+
formData.append("file", file);
|
|
331
|
+
if (additionalData) {
|
|
332
|
+
Object.keys(additionalData).forEach((key) => {
|
|
333
|
+
formData.append(key, additionalData[key]);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const response = await axiosInstance.post(finalUrl, formData, {
|
|
338
|
+
...config,
|
|
339
|
+
headers: {
|
|
340
|
+
...config?.headers,
|
|
341
|
+
"Content-Type": "multipart/form-data"
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
return processResponse(response);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
return processResponse(error);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
return { get, getWithQuery, post, put, patch, remove, bulkDelete, upload };
|
|
289
350
|
}
|
|
290
351
|
|
|
291
352
|
// src/services/actions.ts
|
|
@@ -356,7 +417,7 @@ function useApi(axiosInstance, config) {
|
|
|
356
417
|
initialQuery = { limit: 10 },
|
|
357
418
|
enabled = true,
|
|
358
419
|
refetchAfterChange = true,
|
|
359
|
-
|
|
420
|
+
requestConfig,
|
|
360
421
|
onSuccess,
|
|
361
422
|
onError
|
|
362
423
|
} = config;
|
|
@@ -373,12 +434,15 @@ function useApi(axiosInstance, config) {
|
|
|
373
434
|
const currentQuery = options || queryOptions;
|
|
374
435
|
setState((prev) => ({ ...prev, data: null, loading: true, error: null }));
|
|
375
436
|
const queryString = buildPaginateQuery(currentQuery);
|
|
376
|
-
const result = await apiServices.getWithQuery(queryString, {
|
|
437
|
+
const result = await apiServices.getWithQuery(queryString, {
|
|
438
|
+
cancelTokenKey: endpoint,
|
|
439
|
+
...requestConfig
|
|
440
|
+
});
|
|
377
441
|
setState(result);
|
|
378
442
|
if (!result.success && onError) {
|
|
379
443
|
onError(result.message || "Fetch failed", result.error || void 0);
|
|
380
444
|
}
|
|
381
|
-
}, [apiServices, queryOptions, endpoint, onError]);
|
|
445
|
+
}, [apiServices, queryOptions, endpoint, onError, requestConfig]);
|
|
382
446
|
(0, import_react.useEffect)(() => {
|
|
383
447
|
if (enabled) {
|
|
384
448
|
fetchData();
|
|
@@ -397,6 +461,19 @@ function useApi(axiosInstance, config) {
|
|
|
397
461
|
}
|
|
398
462
|
return result;
|
|
399
463
|
};
|
|
464
|
+
const putItem = async (id, item, options) => {
|
|
465
|
+
setState((prev) => ({ ...prev, loading: true }));
|
|
466
|
+
const result = await apiServices.put(id, item, options);
|
|
467
|
+
if (result.success) {
|
|
468
|
+
if (refetchAfterChange) await fetchData();
|
|
469
|
+
else setState((prev) => ({ ...prev, loading: false }));
|
|
470
|
+
if (onSuccess) onSuccess(result.message || "Item replaced successfully!", result.data);
|
|
471
|
+
} else {
|
|
472
|
+
setState((prev) => ({ ...prev, loading: false, error: result.error }));
|
|
473
|
+
if (onError) onError(result.message || "Replace failed", result.error || void 0);
|
|
474
|
+
}
|
|
475
|
+
return result;
|
|
476
|
+
};
|
|
400
477
|
const updateItem = async (id, updatedItem, options) => {
|
|
401
478
|
setState((prev) => ({ ...prev, loading: true }));
|
|
402
479
|
const result = await apiServices.patch(id, updatedItem, options);
|
|
@@ -423,19 +500,39 @@ function useApi(axiosInstance, config) {
|
|
|
423
500
|
}
|
|
424
501
|
return result;
|
|
425
502
|
};
|
|
503
|
+
const bulkDeleteItem = async (ids, options) => {
|
|
504
|
+
setState((prev) => ({ ...prev, loading: true }));
|
|
505
|
+
const result = await apiServices.bulkDelete(ids, options);
|
|
506
|
+
if (result.success) {
|
|
507
|
+
if (refetchAfterChange) await fetchData();
|
|
508
|
+
else setState((prev) => ({ ...prev, loading: false }));
|
|
509
|
+
if (onSuccess) onSuccess(result.message || "Items deleted successfully!");
|
|
510
|
+
} else {
|
|
511
|
+
setState((prev) => ({ ...prev, loading: false, error: result.error }));
|
|
512
|
+
if (onError) onError(result.message || "Bulk delete failed", result.error || void 0);
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
515
|
+
};
|
|
516
|
+
const uploadFile = async (file, additionalData, options) => {
|
|
517
|
+
setState((prev) => ({ ...prev, loading: true }));
|
|
518
|
+
const result = await apiServices.upload(file, additionalData, options);
|
|
519
|
+
if (result.success) {
|
|
520
|
+
if (refetchAfterChange && options?.refetch !== false) await fetchData();
|
|
521
|
+
else setState((prev) => ({ ...prev, loading: false }));
|
|
522
|
+
if (onSuccess) onSuccess(result.message || "File uploaded successfully!", result.data);
|
|
523
|
+
} else {
|
|
524
|
+
setState((prev) => ({ ...prev, loading: false, error: result.error }));
|
|
525
|
+
if (onError) onError(result.message || "Upload failed", result.error || void 0);
|
|
526
|
+
}
|
|
527
|
+
return result;
|
|
528
|
+
};
|
|
426
529
|
const setPage = (page) => setQueryOptions((prev) => ({ ...prev, page }));
|
|
427
530
|
const setLimit = (limit) => setQueryOptions((prev) => ({ ...prev, limit, page: 1 }));
|
|
428
531
|
const setSearchTerm = (search) => setQueryOptions((prev) => ({ ...prev, search, page: 1 }));
|
|
429
532
|
const setSorting = (sortBy) => setQueryOptions((prev) => ({ ...prev, sortBy }));
|
|
430
533
|
const setFilters = (filter) => setQueryOptions((prev) => ({ ...prev, filter, page: 1 }));
|
|
431
534
|
const setQueryParam = (key, value) => {
|
|
432
|
-
setQueryOptions((prev) => {
|
|
433
|
-
const newQuery = { ...prev, [key]: value };
|
|
434
|
-
if (key !== "page") {
|
|
435
|
-
newQuery.page = 1;
|
|
436
|
-
}
|
|
437
|
-
return newQuery;
|
|
438
|
-
});
|
|
535
|
+
setQueryOptions((prev) => ({ ...prev, [key]: value, page: key !== "page" ? 1 : prev.page }));
|
|
439
536
|
};
|
|
440
537
|
const resetQuery = () => setQueryOptions(initialQuery);
|
|
441
538
|
return {
|
|
@@ -444,8 +541,14 @@ function useApi(axiosInstance, config) {
|
|
|
444
541
|
actions: {
|
|
445
542
|
fetch: fetchData,
|
|
446
543
|
create: createItem,
|
|
544
|
+
put: putItem,
|
|
545
|
+
// <-- NEW
|
|
447
546
|
update: updateItem,
|
|
448
|
-
remove: deleteItem
|
|
547
|
+
remove: deleteItem,
|
|
548
|
+
bulkRemove: bulkDeleteItem,
|
|
549
|
+
// <-- NEW
|
|
550
|
+
upload: uploadFile
|
|
551
|
+
// <-- NEW
|
|
449
552
|
},
|
|
450
553
|
query: {
|
|
451
554
|
options: queryOptions,
|
|
@@ -456,7 +559,6 @@ function useApi(axiosInstance, config) {
|
|
|
456
559
|
setSorting,
|
|
457
560
|
setFilters,
|
|
458
561
|
setQueryParam,
|
|
459
|
-
// <-- NEW
|
|
460
562
|
reset: resetQuery
|
|
461
563
|
}
|
|
462
564
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -61,34 +61,40 @@ function createApiClient(config) {
|
|
|
61
61
|
headers: { "Content-Type": "application/json", ...headers },
|
|
62
62
|
withCredentials
|
|
63
63
|
});
|
|
64
|
-
let
|
|
64
|
+
let isRefreshing = false;
|
|
65
|
+
let failedQueue = [];
|
|
66
|
+
const processQueue = (error, token = null) => {
|
|
67
|
+
failedQueue.forEach((prom) => {
|
|
68
|
+
if (error) {
|
|
69
|
+
prom.reject(error);
|
|
70
|
+
} else {
|
|
71
|
+
prom.resolve(token);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
failedQueue = [];
|
|
75
|
+
};
|
|
65
76
|
axiosInstance.interceptors.request.use(async (req) => {
|
|
66
77
|
req.headers["X-Request-ID"] = uuidv4();
|
|
67
78
|
if (req.isPublic) {
|
|
68
|
-
console.log(`[API Core] Skipping token for public request: ${req.url}`);
|
|
69
79
|
return req;
|
|
70
80
|
}
|
|
71
|
-
if (tokenRefreshPromise) {
|
|
72
|
-
await tokenRefreshPromise;
|
|
73
|
-
}
|
|
74
81
|
let tokens = await tokenManager.getTokens();
|
|
75
82
|
const now = Date.now();
|
|
76
83
|
const tokenBuffer = 60 * 1e3;
|
|
77
|
-
if (tokens.accessToken && tokens.expiresAt && tokens.expiresAt - now < tokenBuffer) {
|
|
78
|
-
if (
|
|
84
|
+
if (tokens.accessToken && tokens.expiresAt && tokens.expiresAt - now < tokenBuffer && !isRefreshing) {
|
|
85
|
+
if (config.refreshTokenConfig) {
|
|
79
86
|
console.log("[API Core] Proactive token refresh initiated.");
|
|
80
|
-
|
|
87
|
+
isRefreshing = true;
|
|
88
|
+
try {
|
|
89
|
+
const newTokens = await refreshToken(config, tokenManager);
|
|
90
|
+
if (newTokens) tokens = newTokens;
|
|
91
|
+
} finally {
|
|
92
|
+
isRefreshing = false;
|
|
93
|
+
}
|
|
81
94
|
}
|
|
82
|
-
const newTokens = await tokenRefreshPromise;
|
|
83
|
-
tokenRefreshPromise = null;
|
|
84
|
-
if (newTokens) tokens = newTokens;
|
|
85
95
|
}
|
|
86
96
|
if (tokens.accessToken && !tokenManager.isHttpOnly()) {
|
|
87
|
-
|
|
88
|
-
req.headers.Authorization = `${tokenType} ${tokens.accessToken}`;
|
|
89
|
-
console.log(`[API Core] Token attached to request: ${req.url}`);
|
|
90
|
-
} else {
|
|
91
|
-
console.warn(`[API Core] No token attached for request: ${req.url}`);
|
|
97
|
+
req.headers.Authorization = `${tokens.tokenType || "Bearer"} ${tokens.accessToken}`;
|
|
92
98
|
}
|
|
93
99
|
return req;
|
|
94
100
|
}, (error) => Promise.reject(error));
|
|
@@ -96,19 +102,34 @@ function createApiClient(config) {
|
|
|
96
102
|
(response) => response,
|
|
97
103
|
async (error) => {
|
|
98
104
|
const originalRequest = error.config;
|
|
99
|
-
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
if (error.response?.status === 401 && !originalRequest._retry && config.refreshTokenConfig) {
|
|
106
|
+
if (isRefreshing) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
failedQueue.push({ resolve, reject });
|
|
109
|
+
}).then((token) => {
|
|
110
|
+
if (token && !tokenManager.isHttpOnly()) {
|
|
111
|
+
originalRequest.headers["Authorization"] = `Bearer ${token}`;
|
|
112
|
+
}
|
|
113
|
+
return axiosInstance(originalRequest);
|
|
114
|
+
});
|
|
104
115
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
originalRequest._retry = true;
|
|
117
|
+
isRefreshing = true;
|
|
118
|
+
try {
|
|
119
|
+
const newTokens = await refreshToken(config, tokenManager);
|
|
120
|
+
if (!newTokens || !newTokens.accessToken) {
|
|
121
|
+
throw new Error("Token refresh failed to produce a new access token.");
|
|
122
|
+
}
|
|
123
|
+
processQueue(null, newTokens.accessToken);
|
|
124
|
+
if (!tokenManager.isHttpOnly()) {
|
|
125
|
+
originalRequest.headers["Authorization"] = `${newTokens.tokenType || "Bearer"} ${newTokens.accessToken}`;
|
|
110
126
|
}
|
|
111
127
|
return axiosInstance(originalRequest);
|
|
128
|
+
} catch (refreshError) {
|
|
129
|
+
processQueue(refreshError, null);
|
|
130
|
+
return Promise.reject(refreshError);
|
|
131
|
+
} finally {
|
|
132
|
+
isRefreshing = false;
|
|
112
133
|
}
|
|
113
134
|
}
|
|
114
135
|
const enhancedError = {
|
|
@@ -225,6 +246,15 @@ function createApiServices(axiosInstance, endpoint) {
|
|
|
225
246
|
return processResponse(error);
|
|
226
247
|
}
|
|
227
248
|
};
|
|
249
|
+
const put = async (id, data, config) => {
|
|
250
|
+
const finalUrl = config?.endpoint || `${endpoint}/${id}`;
|
|
251
|
+
try {
|
|
252
|
+
const response = await axiosInstance.put(finalUrl, data, config);
|
|
253
|
+
return processResponse(response);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return processResponse(error);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
228
258
|
const patch = async (id, data, config) => {
|
|
229
259
|
const finalUrl = config?.endpoint || `${endpoint}/${id}`;
|
|
230
260
|
try {
|
|
@@ -243,7 +273,38 @@ function createApiServices(axiosInstance, endpoint) {
|
|
|
243
273
|
return processResponse(error);
|
|
244
274
|
}
|
|
245
275
|
};
|
|
246
|
-
|
|
276
|
+
const bulkDelete = async (ids, config) => {
|
|
277
|
+
const finalUrl = config?.endpoint || `${endpoint}`;
|
|
278
|
+
try {
|
|
279
|
+
const response = await axiosInstance.delete(finalUrl, { data: { ids }, ...config });
|
|
280
|
+
return processResponse(response);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return processResponse(error);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
const upload = async (file, additionalData, config) => {
|
|
286
|
+
const finalUrl = config?.endpoint || `${endpoint}`;
|
|
287
|
+
const formData = new FormData();
|
|
288
|
+
formData.append("file", file);
|
|
289
|
+
if (additionalData) {
|
|
290
|
+
Object.keys(additionalData).forEach((key) => {
|
|
291
|
+
formData.append(key, additionalData[key]);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const response = await axiosInstance.post(finalUrl, formData, {
|
|
296
|
+
...config,
|
|
297
|
+
headers: {
|
|
298
|
+
...config?.headers,
|
|
299
|
+
"Content-Type": "multipart/form-data"
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
return processResponse(response);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return processResponse(error);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
return { get, getWithQuery, post, put, patch, remove, bulkDelete, upload };
|
|
247
308
|
}
|
|
248
309
|
|
|
249
310
|
// src/services/actions.ts
|
|
@@ -314,7 +375,7 @@ function useApi(axiosInstance, config) {
|
|
|
314
375
|
initialQuery = { limit: 10 },
|
|
315
376
|
enabled = true,
|
|
316
377
|
refetchAfterChange = true,
|
|
317
|
-
|
|
378
|
+
requestConfig,
|
|
318
379
|
onSuccess,
|
|
319
380
|
onError
|
|
320
381
|
} = config;
|
|
@@ -331,12 +392,15 @@ function useApi(axiosInstance, config) {
|
|
|
331
392
|
const currentQuery = options || queryOptions;
|
|
332
393
|
setState((prev) => ({ ...prev, data: null, loading: true, error: null }));
|
|
333
394
|
const queryString = buildPaginateQuery(currentQuery);
|
|
334
|
-
const result = await apiServices.getWithQuery(queryString, {
|
|
395
|
+
const result = await apiServices.getWithQuery(queryString, {
|
|
396
|
+
cancelTokenKey: endpoint,
|
|
397
|
+
...requestConfig
|
|
398
|
+
});
|
|
335
399
|
setState(result);
|
|
336
400
|
if (!result.success && onError) {
|
|
337
401
|
onError(result.message || "Fetch failed", result.error || void 0);
|
|
338
402
|
}
|
|
339
|
-
}, [apiServices, queryOptions, endpoint, onError]);
|
|
403
|
+
}, [apiServices, queryOptions, endpoint, onError, requestConfig]);
|
|
340
404
|
useEffect(() => {
|
|
341
405
|
if (enabled) {
|
|
342
406
|
fetchData();
|
|
@@ -355,6 +419,19 @@ function useApi(axiosInstance, config) {
|
|
|
355
419
|
}
|
|
356
420
|
return result;
|
|
357
421
|
};
|
|
422
|
+
const putItem = async (id, item, options) => {
|
|
423
|
+
setState((prev) => ({ ...prev, loading: true }));
|
|
424
|
+
const result = await apiServices.put(id, item, options);
|
|
425
|
+
if (result.success) {
|
|
426
|
+
if (refetchAfterChange) await fetchData();
|
|
427
|
+
else setState((prev) => ({ ...prev, loading: false }));
|
|
428
|
+
if (onSuccess) onSuccess(result.message || "Item replaced successfully!", result.data);
|
|
429
|
+
} else {
|
|
430
|
+
setState((prev) => ({ ...prev, loading: false, error: result.error }));
|
|
431
|
+
if (onError) onError(result.message || "Replace failed", result.error || void 0);
|
|
432
|
+
}
|
|
433
|
+
return result;
|
|
434
|
+
};
|
|
358
435
|
const updateItem = async (id, updatedItem, options) => {
|
|
359
436
|
setState((prev) => ({ ...prev, loading: true }));
|
|
360
437
|
const result = await apiServices.patch(id, updatedItem, options);
|
|
@@ -381,19 +458,39 @@ function useApi(axiosInstance, config) {
|
|
|
381
458
|
}
|
|
382
459
|
return result;
|
|
383
460
|
};
|
|
461
|
+
const bulkDeleteItem = async (ids, options) => {
|
|
462
|
+
setState((prev) => ({ ...prev, loading: true }));
|
|
463
|
+
const result = await apiServices.bulkDelete(ids, options);
|
|
464
|
+
if (result.success) {
|
|
465
|
+
if (refetchAfterChange) await fetchData();
|
|
466
|
+
else setState((prev) => ({ ...prev, loading: false }));
|
|
467
|
+
if (onSuccess) onSuccess(result.message || "Items deleted successfully!");
|
|
468
|
+
} else {
|
|
469
|
+
setState((prev) => ({ ...prev, loading: false, error: result.error }));
|
|
470
|
+
if (onError) onError(result.message || "Bulk delete failed", result.error || void 0);
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
};
|
|
474
|
+
const uploadFile = async (file, additionalData, options) => {
|
|
475
|
+
setState((prev) => ({ ...prev, loading: true }));
|
|
476
|
+
const result = await apiServices.upload(file, additionalData, options);
|
|
477
|
+
if (result.success) {
|
|
478
|
+
if (refetchAfterChange && options?.refetch !== false) await fetchData();
|
|
479
|
+
else setState((prev) => ({ ...prev, loading: false }));
|
|
480
|
+
if (onSuccess) onSuccess(result.message || "File uploaded successfully!", result.data);
|
|
481
|
+
} else {
|
|
482
|
+
setState((prev) => ({ ...prev, loading: false, error: result.error }));
|
|
483
|
+
if (onError) onError(result.message || "Upload failed", result.error || void 0);
|
|
484
|
+
}
|
|
485
|
+
return result;
|
|
486
|
+
};
|
|
384
487
|
const setPage = (page) => setQueryOptions((prev) => ({ ...prev, page }));
|
|
385
488
|
const setLimit = (limit) => setQueryOptions((prev) => ({ ...prev, limit, page: 1 }));
|
|
386
489
|
const setSearchTerm = (search) => setQueryOptions((prev) => ({ ...prev, search, page: 1 }));
|
|
387
490
|
const setSorting = (sortBy) => setQueryOptions((prev) => ({ ...prev, sortBy }));
|
|
388
491
|
const setFilters = (filter) => setQueryOptions((prev) => ({ ...prev, filter, page: 1 }));
|
|
389
492
|
const setQueryParam = (key, value) => {
|
|
390
|
-
setQueryOptions((prev) => {
|
|
391
|
-
const newQuery = { ...prev, [key]: value };
|
|
392
|
-
if (key !== "page") {
|
|
393
|
-
newQuery.page = 1;
|
|
394
|
-
}
|
|
395
|
-
return newQuery;
|
|
396
|
-
});
|
|
493
|
+
setQueryOptions((prev) => ({ ...prev, [key]: value, page: key !== "page" ? 1 : prev.page }));
|
|
397
494
|
};
|
|
398
495
|
const resetQuery = () => setQueryOptions(initialQuery);
|
|
399
496
|
return {
|
|
@@ -402,8 +499,14 @@ function useApi(axiosInstance, config) {
|
|
|
402
499
|
actions: {
|
|
403
500
|
fetch: fetchData,
|
|
404
501
|
create: createItem,
|
|
502
|
+
put: putItem,
|
|
503
|
+
// <-- NEW
|
|
405
504
|
update: updateItem,
|
|
406
|
-
remove: deleteItem
|
|
505
|
+
remove: deleteItem,
|
|
506
|
+
bulkRemove: bulkDeleteItem,
|
|
507
|
+
// <-- NEW
|
|
508
|
+
upload: uploadFile
|
|
509
|
+
// <-- NEW
|
|
407
510
|
},
|
|
408
511
|
query: {
|
|
409
512
|
options: queryOptions,
|
|
@@ -414,7 +517,6 @@ function useApi(axiosInstance, config) {
|
|
|
414
517
|
setSorting,
|
|
415
518
|
setFilters,
|
|
416
519
|
setQueryParam,
|
|
417
|
-
// <-- NEW
|
|
418
520
|
reset: resetQuery
|
|
419
521
|
}
|
|
420
522
|
};
|