@feardread/feature-factory 4.0.1 → 4.0.3

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