@feardread/feature-factory 3.0.2 → 4.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feardread/feature-factory",
3
- "version": "3.0.2",
3
+ "version": "4.0.2",
4
4
  "description": "Library to interact with redux toolkit and reduce boilerplate / repeated code",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -1,72 +1,537 @@
1
- import axios from "axios";
2
- import qs from "qs";
3
- import CacheFactory from "./cache";
1
+ import axios from 'axios';
2
+ import qs from 'qs';
3
+ import CacheFactory from './cache';
4
4
 
5
+ /**
6
+ * Environment-based configuration
7
+ */
8
+ const CONFIG = {
9
+ // API Base URLs
10
+ baseUrls: {
11
+ production: process.env.REACT_APP_API_BASE_URL_PROD || 'https://fear.master.com/fear/api/',
12
+ development: process.env.REACT_APP_API_BASE_URL_DEV || 'http://localhost:4000/fear/api/',
13
+ test: process.env.REACT_APP_API_BASE_URL_TEST || 'http://localhost:3001/fear/api/',
14
+ },
15
+
16
+ // Token configuration
17
+ tokenNames: {
18
+ bearer: 'Authorization',
19
+ custom: process.env.REACT_APP_JWT_TOKEN_HEADER || 'x-token',
20
+ },
21
+
22
+ // Cache keys
23
+ cacheKeys: {
24
+ auth: 'auth',
25
+ refreshToken: 'refresh_token',
26
+ userPrefs: 'user_preferences',
27
+ },
28
+
29
+ // Request configuration
30
+ timeout: parseInt(process.env.REACT_APP_API_TIMEOUT) || 30000,
31
+ retryAttempts: parseInt(process.env.REACT_APP_API_RETRY_ATTEMPTS) || 3,
32
+ retryDelay: parseInt(process.env.REACT_APP_API_RETRY_DELAY) || 1000,
33
+ };
5
34
 
6
- const API_BASE_URL = (process.env.NODE_ENV === "production")
7
- ? "http://fear.master.com:4000/fear/api/"
8
- : "http://localhost:4000/fear/api/";
35
+ /**
36
+ * Gets the appropriate base URL for the current environment
37
+ * @returns {string} Base URL
38
+ */
39
+ const getBaseUrl = () => {
40
+ const env = process.env.NODE_ENV || 'development';
41
+ return CONFIG.baseUrls[env] || CONFIG.baseUrls.development;
42
+ };
9
43
 
10
- const ACCESS_TOKEN_NAME = (process.env.JWT_TOKEN)
11
- ? process.env.JWT_TOKEN
12
- : "x-token";
44
+ /**
45
+ * Gets authentication data from cache
46
+ * @returns {Object|null} Auth data or null
47
+ */
48
+ const getAuthData = async () => {
49
+ try {
50
+ const authData = await CacheFactory.local.get(CONFIG.cacheKeys.auth);
51
+ return authData && typeof authData === 'object' ? authData : null;
52
+ } catch (error) {
53
+ console.warn('Failed to retrieve auth data:', error);
54
+ return null;
55
+ }
56
+ };
13
57
 
14
- const instance = axios.create({
15
- baseURL: `${API_BASE_URL}`,
58
+ /**
59
+ * Checks if token is expired
60
+ * @param {string} token - JWT token
61
+ * @returns {boolean} Whether token is expired
62
+ */
63
+ const isTokenExpired = (token) => {
64
+ if (!token) return true;
65
+
66
+ try {
67
+ const payload = JSON.parse(atob(token.split('.')[1]));
68
+ const currentTime = Date.now() / 1000;
69
+ return payload.exp < currentTime;
70
+ } catch (error) {
71
+ return true;
72
+ }
73
+ };
74
+
75
+ /**
76
+ * Refreshes the authentication token
77
+ * @returns {Promise<string|null>} New token or null if refresh failed
78
+ */
79
+ const refreshAuthToken = async () => {
80
+ try {
81
+ const authData = await getAuthData();
82
+ const refreshToken = authData?.refreshToken || await CacheFactory.local.get(CONFIG.cacheKeys.refreshToken);
83
+
84
+ if (!refreshToken) {
85
+ throw new Error('No refresh token available');
86
+ }
87
+
88
+ const response = await axios.post(`${getBaseUrl()}auth/refresh`, {
89
+ refreshToken,
90
+ });
91
+
92
+ const newAuthData = {
93
+ token: response.data.token,
94
+ refreshToken: response.data.refreshToken || refreshToken,
95
+ user: response.data.user,
96
+ expiresAt: response.data.expiresAt,
97
+ };
98
+
99
+ await CacheFactory.local.set(CONFIG.cacheKeys.auth, newAuthData);
100
+ return newAuthData.token;
101
+ } catch (error) {
102
+ console.error('Token refresh failed:', error);
103
+ await clearAuthData();
104
+ return null;
105
+ }
106
+ };
107
+
108
+ /**
109
+ * Clears authentication data from cache
110
+ */
111
+ const clearAuthData = async () => {
112
+ try {
113
+ await Promise.all([
114
+ CacheFactory.local.remove(CONFIG.cacheKeys.auth),
115
+ CacheFactory.local.remove(CONFIG.cacheKeys.refreshToken),
116
+ ]);
117
+ } catch (error) {
118
+ console.warn('Failed to clear auth data:', error);
119
+ }
120
+ };
121
+
122
+ /**
123
+ * Creates retry delay with exponential backoff
124
+ * @param {number} attempt - Current attempt number
125
+ * @returns {number} Delay in milliseconds
126
+ */
127
+ const getRetryDelay = (attempt) => {
128
+ return CONFIG.retryDelay * Math.pow(2, attempt - 1);
129
+ };
130
+
131
+ /**
132
+ * Standardized error formatter
133
+ * @param {Object} error - Axios error object
134
+ * @returns {Object} Formatted error
135
+ */
136
+ const formatError = (error) => {
137
+ const baseError = {
138
+ message: 'An unknown error occurred',
139
+ status: null,
140
+ code: null,
141
+ data: null,
142
+ timestamp: new Date().toISOString(),
143
+ };
144
+
145
+ if (error.response) {
146
+ // Server responded with error status
147
+ return {
148
+ ...baseError,
149
+ message: error.response.data?.message || `HTTP Error ${error.response.status}`,
150
+ status: error.response.status,
151
+ code: error.response.data?.code || error.code,
152
+ data: error.response.data,
153
+ };
154
+ }
155
+
156
+ if (error.request) {
157
+ // Request made but no response received
158
+ return {
159
+ ...baseError,
160
+ message: 'Network error - no response received',
161
+ code: 'NETWORK_ERROR',
162
+ };
163
+ }
164
+
165
+ // Something else happened
166
+ return {
167
+ ...baseError,
168
+ message: error.message || 'Request configuration error',
169
+ code: error.code || 'CONFIG_ERROR',
170
+ };
171
+ };
172
+
173
+ /**
174
+ * Request interceptor with retry logic
175
+ * @param {Object} config - Axios config
176
+ * @returns {Promise<Object>} Modified config
177
+ */
178
+ const requestInterceptor = async (config) => {
179
+ try {
180
+ // Set retry metadata
181
+ config.metadata = { startTime: Date.now() };
182
+ config._retry = config._retry || 0;
183
+
184
+ // Get authentication data
185
+ const authData = await getAuthData();
186
+ let token = authData?.token;
187
+
188
+ // Check if token needs refresh
189
+ if (token && isTokenExpired(token) && config._retry === 0) {
190
+ token = await refreshAuthToken();
191
+ }
192
+
193
+ // Set authorization headers
194
+ if (token) {
195
+ config.headers[CONFIG.tokenNames.bearer] = `Bearer ${token}`;
196
+ config.headers[CONFIG.tokenNames.custom] = token;
197
+ }
198
+
199
+ // Add request ID for tracking
200
+ config.headers['X-Request-ID'] = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
201
+
202
+ // Add client info
203
+ config.headers['X-Client-Version'] = process.env.REACT_APP_VERSION || '1.0.0';
204
+ config.headers['X-Client-Platform'] = 'web';
205
+
206
+ return config;
207
+ } catch (error) {
208
+ console.error('Request interceptor error:', error);
209
+ return config;
210
+ }
211
+ };
212
+
213
+ /**
214
+ * Request error interceptor
215
+ * @param {Object} error - Request error
216
+ * @returns {Promise<Object>} Rejected promise
217
+ */
218
+ const requestErrorInterceptor = (error) => {
219
+ console.error('Request setup error:', error);
220
+ return Promise.reject(formatError(error));
221
+ };
222
+
223
+ /**
224
+ * Response interceptor with success handling
225
+ * @param {Object} response - Axios response
226
+ * @returns {Object} Processed response
227
+ */
228
+ const responseInterceptor = (response) => {
229
+ // Add response metadata
230
+ response.metadata = {
231
+ responseTime: Date.now() - (response.config.metadata?.startTime || Date.now()),
232
+ requestId: response.config.headers['X-Request-ID'],
233
+ };
234
+
235
+ // Log successful responses in development
236
+ if (process.env.NODE_ENV === 'development') {
237
+ console.log(`✅ API Success [${response.status}]:`, {
238
+ url: response.config.url,
239
+ method: response.config.method?.toUpperCase(),
240
+ responseTime: response.metadata.responseTime,
241
+ requestId: response.metadata.requestId,
242
+ });
243
+ }
244
+
245
+ // Validate response structure
246
+ if (response.data && typeof response.data === 'object') {
247
+ // Handle different success status codes
248
+ if ([200, 201, 202, 204].includes(response.status)) {
249
+ return response;
250
+ }
251
+ }
252
+
253
+ // Handle edge cases
254
+ if (response.status >= 200 && response.status < 300) {
255
+ return response;
256
+ }
257
+
258
+ // Unexpected status code
259
+ return Promise.reject(formatError({
260
+ response,
261
+ message: `Unexpected response status: ${response.status}`,
262
+ }));
263
+ };
264
+
265
+ /**
266
+ * Response error interceptor with retry and auth handling
267
+ * @param {Object} error - Response error
268
+ * @returns {Promise<Object>} Retry attempt or rejected promise
269
+ */
270
+ const responseErrorInterceptor = async (error) => {
271
+ const originalRequest = error.config;
272
+
273
+ // Format error for consistent handling
274
+ const formattedError = formatError(error);
275
+
276
+ // Log errors in development
277
+ if (process.env.NODE_ENV === 'development') {
278
+ console.error(`❌ API Error [${formattedError.status}]:`, {
279
+ url: originalRequest?.url,
280
+ method: originalRequest?.method?.toUpperCase(),
281
+ message: formattedError.message,
282
+ requestId: originalRequest?.headers['X-Request-ID'],
283
+ });
284
+ }
285
+
286
+ // Handle authentication errors
287
+ if (formattedError.status === 401 && !originalRequest._isRetryingAuth) {
288
+ originalRequest._isRetryingAuth = true;
289
+
290
+ try {
291
+ const newToken = await refreshAuthToken();
292
+ if (newToken) {
293
+ // Retry original request with new token
294
+ originalRequest.headers[CONFIG.tokenNames.bearer] = `Bearer ${newToken}`;
295
+ originalRequest.headers[CONFIG.tokenNames.custom] = newToken;
296
+ return apiInstance(originalRequest);
297
+ }
298
+ } catch (refreshError) {
299
+ console.error('Auth refresh failed:', refreshError);
300
+ }
301
+
302
+ // Clear auth data and redirect to login
303
+ await clearAuthData();
304
+
305
+ // Dispatch auth failure event
306
+ if (typeof window !== 'undefined') {
307
+ window.dispatchEvent(new CustomEvent('auth:failure', {
308
+ detail: { error: formattedError }
309
+ }));
310
+ }
311
+ }
312
+
313
+ // Handle retryable errors
314
+ const isRetryable = [408, 429, 500, 502, 503, 504].includes(formattedError.status) ||
315
+ formattedError.code === 'NETWORK_ERROR';
316
+
317
+ if (isRetryable && originalRequest._retry < CONFIG.retryAttempts) {
318
+ originalRequest._retry += 1;
319
+
320
+ const delay = getRetryDelay(originalRequest._retry);
321
+ console.log(`⏳ Retrying request (${originalRequest._retry}/${CONFIG.retryAttempts}) in ${delay}ms`);
322
+
323
+ await new Promise(resolve => setTimeout(resolve, delay));
324
+ return apiInstance(originalRequest);
325
+ }
326
+
327
+ // Handle rate limiting
328
+ if (formattedError.status === 429) {
329
+ const retryAfter = error.response?.headers['retry-after'];
330
+ if (retryAfter) {
331
+ formattedError.retryAfter = parseInt(retryAfter) * 1000;
332
+ }
333
+ }
334
+
335
+ return Promise.reject(formattedError);
336
+ };
337
+
338
+ /**
339
+ * Creates the main API instance
340
+ */
341
+ const createApiInstance = () => {
342
+ const instance = axios.create({
343
+ baseURL: getBaseUrl(),
344
+ timeout: CONFIG.timeout,
16
345
  headers: {
17
- Accept: "application/json",
18
- "Content-Type": "application/json",
346
+ 'Accept': 'application/json',
347
+ 'Content-Type': 'application/json',
19
348
  },
20
- paramsSerializer: (params) => {
21
- return qs.stringify(params, { indexes: false });
349
+ paramsSerializer: {
350
+ serialize: (params) => qs.stringify(params, {
351
+ indices: false,
352
+ skipNulls: true,
353
+ arrayFormat: 'brackets',
354
+ }),
22
355
  },
23
- credentials: true
24
- //httpsAgent: new https.Agent({ rejectUnauthorized: false })
25
- });
26
-
27
- instance.interceptors.request.use(
28
- (config) => {
29
- const isAuth = CacheFactory.local.get("auth") ? CacheFactory.local.get("auth") : null;
30
- let token = isAuth !== null ? isAuth.token : "";
356
+ withCredentials: true,
357
+ // Disable automatic JSON parsing for better error handling
358
+ transformResponse: [
359
+ (data) => {
360
+ try {
361
+ return JSON.parse(data);
362
+ } catch (error) {
363
+ return data;
364
+ }
365
+ }
366
+ ],
367
+ });
368
+
369
+ // Add interceptors
370
+ instance.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
371
+ instance.interceptors.response.use(responseInterceptor, responseErrorInterceptor);
372
+
373
+ return instance;
374
+ };
375
+
376
+ // Create the main API instance
377
+ const apiInstance = createApiInstance();
378
+
379
+ /**
380
+ * API utility methods
381
+ */
382
+ const API = {
383
+ // Main axios instance
384
+ instance: apiInstance,
385
+
386
+ // Direct access to axios methods
387
+ get: apiInstance.get.bind(apiInstance),
388
+ post: apiInstance.post.bind(apiInstance),
389
+ put: apiInstance.put.bind(apiInstance),
390
+ patch: apiInstance.patch.bind(apiInstance),
391
+ delete: apiInstance.delete.bind(apiInstance),
392
+ head: apiInstance.head.bind(apiInstance),
393
+ options: apiInstance.options.bind(apiInstance),
394
+
395
+ // Utility methods
396
+ /**
397
+ * Makes a request with custom configuration
398
+ * @param {Object} config - Request configuration
399
+ * @returns {Promise<Object>} Response
400
+ */
401
+ request: (config) => apiInstance.request(config),
402
+
403
+ /**
404
+ * Creates a new API instance with custom configuration
405
+ * @param {Object} customConfig - Custom axios configuration
406
+ * @returns {Object} New API instance
407
+ */
408
+ create: (customConfig = {}) => {
409
+ const customInstance = axios.create({
410
+ ...apiInstance.defaults,
411
+ ...customConfig,
412
+ });
31
413
 
32
- config.headers = {
33
- Authorization: `Bearer ${token}`,
34
- [ACCESS_TOKEN_NAME]: token
35
- };
414
+ customInstance.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
415
+ customInstance.interceptors.response.use(responseInterceptor, responseErrorInterceptor);
416
+
417
+ return customInstance;
418
+ },
36
419
 
37
- return config;
38
- },
39
- (error) => { Promise.reject(error) }
40
- );
420
+ /**
421
+ * Sets authentication token
422
+ * @param {string} token - Auth token
423
+ * @param {Object} userData - User data
424
+ * @returns {Promise<boolean>} Success status
425
+ */
426
+ setAuth: async (token, userData = {}) => {
427
+ try {
428
+ const authData = {
429
+ token,
430
+ user: userData,
431
+ timestamp: Date.now(),
432
+ expiresAt: userData.expiresAt,
433
+ };
434
+
435
+ await CacheFactory.local.set(CONFIG.cacheKeys.auth, authData);
436
+ return true;
437
+ } catch (error) {
438
+ console.error('Failed to set auth:', error);
439
+ return false;
440
+ }
441
+ },
41
442
 
42
- instance.interceptors.response.use(
43
- (response) => {
44
- console.log("API RES :: ", response);
45
- const messages = response.data.message;
443
+ /**
444
+ * Clears authentication
445
+ * @returns {Promise<boolean>} Success status
446
+ */
447
+ clearAuth: async () => {
448
+ try {
449
+ await clearAuthData();
450
+ return true;
451
+ } catch (error) {
452
+ console.error('Failed to clear auth:', error);
453
+ return false;
454
+ }
455
+ },
46
456
 
47
- if (response.status === 200 || 203) {
48
- return response;
49
- }
50
- if (messages) return Promise.reject({ messages: [messages] });
51
-
52
- return Promise.reject({ messages: ["got errors"] });
53
- },
54
- (error) => {
55
- console.log("API ERROR :: ", error);
56
- if (error.response) {
57
- if (error.response.status === 401) {
58
- CacheFactory.local.remove("auth");
59
- return Promise.reject(error.response);
60
- }
61
- if (error.response.status === 500) {
62
- return Promise.reject(error.response);
63
- }
64
- }
65
- return Promise.reject(error);
457
+ /**
458
+ * Gets current auth status
459
+ * @returns {Promise<Object>} Auth status
460
+ */
461
+ getAuthStatus: async () => {
462
+ const authData = await getAuthData();
463
+ const isAuthenticated = !!(authData?.token && !isTokenExpired(authData.token));
464
+
465
+ return {
466
+ isAuthenticated,
467
+ user: authData?.user || null,
468
+ token: authData?.token || null,
469
+ expiresAt: authData?.expiresAt || null,
470
+ };
471
+ },
472
+
473
+ /**
474
+ * Configures global request defaults
475
+ * @param {Object} config - Configuration object
476
+ */
477
+ configure: (config) => {
478
+ Object.assign(apiInstance.defaults, config);
479
+ },
480
+
481
+ /**
482
+ * Health check endpoint
483
+ * @returns {Promise<Object>} Health status
484
+ */
485
+ healthCheck: async () => {
486
+ try {
487
+ const response = await apiInstance.get('health', { timeout: 5000 });
488
+ return {
489
+ status: 'healthy',
490
+ response: response.data,
491
+ timestamp: Date.now(),
492
+ };
493
+ } catch (error) {
494
+ return {
495
+ status: 'unhealthy',
496
+ error: formatError(error),
497
+ timestamp: Date.now(),
498
+ };
66
499
  }
67
- );
500
+ },
501
+
502
+ /**
503
+ * Upload file with progress tracking
504
+ * @param {string} url - Upload URL
505
+ * @param {FormData} formData - Form data with file
506
+ * @param {Function} onProgress - Progress callback
507
+ * @returns {Promise<Object>} Upload response
508
+ */
509
+ uploadFile: (url, formData, onProgress) => {
510
+ return apiInstance.post(url, formData, {
511
+ headers: {
512
+ 'Content-Type': 'multipart/form-data',
513
+ },
514
+ onUploadProgress: (progressEvent) => {
515
+ if (onProgress && progressEvent.total) {
516
+ const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
517
+ onProgress(progress, progressEvent);
518
+ }
519
+ },
520
+ });
521
+ },
68
522
 
523
+ // Configuration access
524
+ config: CONFIG,
525
+ getBaseUrl,
526
+ };
69
527
 
70
- export const API = instance;
528
+ // Add event listener for auth failures (optional)
529
+ if (typeof window !== 'undefined') {
530
+ window.addEventListener('auth:failure', (event) => {
531
+ console.warn('Authentication failed:', event.detail.error);
532
+ // Could trigger a redirect to login page here
533
+ });
534
+ }
71
535
 
536
+ export { API };
72
537
  export default API;