@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.
@@ -1,53 +1,279 @@
1
- import { createAsyncThunk } from "@reduxjs/toolkit"
2
- import API from "./api"
1
+ import { createAsyncThunk } from '@reduxjs/toolkit';
2
+ import API from './api';
3
3
 
4
- // Async Thunk Factory Function
5
- // This factory creates a specific createAsyncThunk for a given 'itemType'
6
- export const ThunkFactory = {
4
+ /**
5
+ * HTTP methods supported by the ThunkFactory
6
+ */
7
+ const HTTP_METHODS = {
8
+ GET: 'GET',
9
+ POST: 'POST',
10
+ PUT: 'PUT',
11
+ PATCH: 'PATCH',
12
+ DELETE: 'DELETE',
13
+ };
7
14
 
8
- create: (entity, prefix) => {
15
+ /**
16
+ * Standard operation prefixes and their configurations
17
+ */
18
+ const STANDARD_OPERATIONS = {
19
+ all: { method: HTTP_METHODS.GET, useParams: false },
20
+ one: { method: HTTP_METHODS.GET, useParams: false, useIdInUrl: true },
21
+ search: { method: HTTP_METHODS.GET, useParams: true },
22
+ create: { method: HTTP_METHODS.POST, useParams: false },
23
+ update: { method: HTTP_METHODS.PUT, useParams: false, useIdInUrl: true },
24
+ patch: { method: HTTP_METHODS.PATCH, useParams: false, useIdInUrl: true },
25
+ delete: { method: HTTP_METHODS.DELETE, useParams: false, useIdInUrl: true },
26
+ };
9
27
 
10
- return (
11
- createAsyncThunk(
28
+ /**
29
+ * Validates input parameters for thunk creation
30
+ * @param {string} entity - The entity name
31
+ * @param {string} prefix - The operation prefix
32
+ * @throws {Error} If validation fails
33
+ */
34
+ const validateThunkParams = (entity, prefix) => {
35
+ if (!entity || typeof entity !== 'string') {
36
+ throw new Error('Entity must be a non-empty string');
37
+ }
38
+ if (!prefix || typeof prefix !== 'string') {
39
+ throw new Error('Prefix must be a non-empty string');
40
+ }
41
+ };
12
42
 
13
- `${entity}/${prefix}`,
43
+ /**
44
+ * Builds the URL for the API call
45
+ * @param {string} entity - The entity name
46
+ * @param {string} prefix - The operation prefix
47
+ * @param {Object} params - Parameters object
48
+ * @param {boolean} useIdInUrl - Whether to append ID to URL
49
+ * @returns {string} The constructed URL
50
+ */
51
+ const buildUrl = (entity, prefix, params = {}, useIdInUrl = false) => {
52
+ let url = `${entity}`;
53
+
54
+ if (useIdInUrl && params?.id) {
55
+ url += `/${params.id}`;
56
+ } else if (!useIdInUrl || prefix !== 'one') {
57
+ url += `/${prefix}`;
58
+ }
59
+
60
+ return url;
61
+ };
62
+
63
+ /**
64
+ * Handles API response consistently
65
+ * @param {Object} response - API response object
66
+ * @returns {any} The response data
67
+ */
68
+ const handleResponse = (response) => {
69
+ // Handle different response structures
70
+ if (response?.data?.result !== undefined) {
71
+ return response.data.result;
72
+ }
73
+ if (response?.data !== undefined) {
74
+ return response.data;
75
+ }
76
+ return response;
77
+ };
14
78
 
15
- async ( params, thunkApi ) => {
16
- let url = `${entity}/${prefix}`;
79
+ /**
80
+ * Handles API errors consistently
81
+ * @param {Error} error - Error object
82
+ * @param {Object} thunkApi - Redux Toolkit thunk API
83
+ * @returns {any} Rejected value
84
+ */
85
+ const handleError = (error, thunkApi) => {
86
+ const errorMessage = error?.response?.data?.message ||
87
+ error?.message ||
88
+ 'An unknown error occurred';
89
+
90
+ const errorPayload = {
91
+ message: errorMessage,
92
+ status: error?.response?.status,
93
+ code: error?.code,
94
+ };
17
95
 
18
- if ( prefix == 'one' ) url = `${entity}/${params.id}`
96
+ return thunkApi.rejectWithValue(errorPayload);
97
+ };
19
98
 
20
- return API.get(url, (prefix == 'search') ? {params} : {})
21
-
22
- .then((response) => response.data.result )
23
-
24
- .catch((error) => thunkApi.rejectWithValue(error.message) )
99
+ /**
100
+ * Creates a generic async thunk with configurable HTTP method and behavior
101
+ * @param {string} entity - The entity name
102
+ * @param {string} prefix - The operation prefix
103
+ * @param {Object} options - Configuration options
104
+ * @returns {Function} The created async thunk
105
+ */
106
+ const createGenericThunk = (entity, prefix, options = {}) => {
107
+ validateThunkParams(entity, prefix);
108
+
109
+ const config = {
110
+ method: HTTP_METHODS.GET,
111
+ useParams: false,
112
+ useIdInUrl: false,
113
+ customUrl: null,
114
+ ...options,
115
+ };
116
+
117
+ return createAsyncThunk(
118
+ `${entity}/${prefix}`,
119
+ async (payload = {}, thunkApi) => {
120
+ try {
121
+ const url = config.customUrl || buildUrl(entity, prefix, payload, config.useIdInUrl);
122
+
123
+ let apiCall;
124
+ const apiConfig = config.useParams ? { params: payload } : {};
125
+
126
+ switch (config.method) {
127
+ case HTTP_METHODS.GET:
128
+ apiCall = API.get(url, apiConfig);
129
+ break;
130
+ case HTTP_METHODS.POST:
131
+ apiCall = API.post(url, payload, apiConfig);
132
+ break;
133
+ case HTTP_METHODS.PUT:
134
+ apiCall = API.put(url, payload, apiConfig);
135
+ break;
136
+ case HTTP_METHODS.PATCH:
137
+ apiCall = API.patch(url, payload, apiConfig);
138
+ break;
139
+ case HTTP_METHODS.DELETE:
140
+ apiCall = API.delete(url, apiConfig);
141
+ break;
142
+ default:
143
+ throw new Error(`Unsupported HTTP method: ${config.method}`);
25
144
  }
26
- )
27
- )
145
+
146
+ const response = await apiCall;
147
+ return handleResponse(response);
148
+ } catch (error) {
149
+ return handleError(error, thunkApi);
150
+ }
151
+ }
152
+ );
153
+ };
154
+
155
+ /**
156
+ * Factory for creating Redux async thunks with standardized patterns
157
+ */
158
+ export const ThunkFactory = {
159
+ /**
160
+ * Creates a GET request thunk (legacy method for backward compatibility)
161
+ * @param {string} entity - The entity name
162
+ * @param {string} prefix - The operation prefix
163
+ * @returns {Function} The created async thunk
164
+ */
165
+ create: (entity, prefix) => {
166
+ validateThunkParams(entity, prefix);
167
+
168
+ const operation = STANDARD_OPERATIONS[prefix];
169
+ if (operation) {
170
+ return createGenericThunk(entity, prefix, operation);
171
+ }
172
+
173
+ // Fallback to original behavior for custom prefixes
174
+ const useParams = prefix === 'search';
175
+ const useIdInUrl = prefix === 'one';
176
+
177
+ return createGenericThunk(entity, prefix, {
178
+ method: HTTP_METHODS.GET,
179
+ useParams,
180
+ useIdInUrl,
181
+ });
28
182
  },
29
183
 
184
+ /**
185
+ * Creates a POST request thunk (legacy method for backward compatibility)
186
+ * @param {string} entity - The entity name
187
+ * @param {string} prefix - The operation prefix
188
+ * @returns {Function} The created async thunk
189
+ */
30
190
  post: (entity, prefix) => {
31
- return (
32
- createAsyncThunk(
33
-
34
- `${entity}/${prefix}`,
191
+ return createGenericThunk(entity, prefix, {
192
+ method: HTTP_METHODS.POST,
193
+ useParams: false,
194
+ });
195
+ },
35
196
 
36
- async ( data, thunkApi ) => {
37
-
38
- return API.post(`${entity}/${prefix}`, data)
39
-
40
- .then((response) => {
197
+ /**
198
+ * Creates a PUT request thunk
199
+ * @param {string} entity - The entity name
200
+ * @param {string} prefix - The operation prefix
201
+ * @returns {Function} The created async thunk
202
+ */
203
+ put: (entity, prefix) => {
204
+ return createGenericThunk(entity, prefix, {
205
+ method: HTTP_METHODS.PUT,
206
+ useIdInUrl: prefix === 'update',
207
+ });
208
+ },
41
209
 
42
- return response.data.result;
210
+ /**
211
+ * Creates a PATCH request thunk
212
+ * @param {string} entity - The entity name
213
+ * @param {string} prefix - The operation prefix
214
+ * @returns {Function} The created async thunk
215
+ */
216
+ patch: (entity, prefix) => {
217
+ return createGenericThunk(entity, prefix, {
218
+ method: HTTP_METHODS.PATCH,
219
+ useIdInUrl: true,
220
+ });
221
+ },
43
222
 
44
- })
45
-
46
- .catch((error) => thunkApi.rejectWithValue(error.message) )
47
- }
48
- )
49
- )
50
- }
51
- }
223
+ /**
224
+ * Creates a DELETE request thunk
225
+ * @param {string} entity - The entity name
226
+ * @param {string} prefix - The operation prefix
227
+ * @returns {Function} The created async thunk
228
+ */
229
+ delete: (entity, prefix) => {
230
+ return createGenericThunk(entity, prefix, {
231
+ method: HTTP_METHODS.DELETE,
232
+ useIdInUrl: true,
233
+ });
234
+ },
235
+
236
+ /**
237
+ * Creates a custom thunk with full control over configuration
238
+ * @param {string} entity - The entity name
239
+ * @param {string} prefix - The operation prefix
240
+ * @param {Object} options - Custom configuration options
241
+ * @returns {Function} The created async thunk
242
+ */
243
+ custom: (entity, prefix, options = {}) => {
244
+ return createGenericThunk(entity, prefix, options);
245
+ },
246
+
247
+ /**
248
+ * Creates standard CRUD thunks for an entity
249
+ * @param {string} entity - The entity name
250
+ * @returns {Object} Object containing all CRUD thunks
251
+ */
252
+ createCrud: (entity) => {
253
+ validateThunkParams(entity, 'crud');
254
+
255
+ return {
256
+ fetchAll: ThunkFactory.create(entity, 'all'),
257
+ fetchOne: ThunkFactory.create(entity, 'one'),
258
+ create: ThunkFactory.post(entity, 'create'),
259
+ update: ThunkFactory.put(entity, 'update'),
260
+ patch: ThunkFactory.patch(entity, 'patch'),
261
+ delete: ThunkFactory.delete(entity, 'delete'),
262
+ search: ThunkFactory.create(entity, 'search'),
263
+ };
264
+ },
265
+
266
+ /**
267
+ * Gets available HTTP methods
268
+ * @returns {Object} HTTP methods object
269
+ */
270
+ getHttpMethods: () => ({ ...HTTP_METHODS }),
271
+
272
+ /**
273
+ * Gets standard operations configuration
274
+ * @returns {Object} Standard operations object
275
+ */
276
+ getStandardOperations: () => ({ ...STANDARD_OPERATIONS }),
277
+ };
52
278
 
53
- export default ThunkFactory;
279
+ export default ThunkFactory;
@@ -1,72 +0,0 @@
1
- import axios from "axios";
2
- import qs from "qs";
3
- import cache from "./cache";
4
-
5
-
6
- const API_BASE_URL = (process.env.NODE_ENV === "production")
7
- ? "http://fear.master.com:4000/fear/api/"
8
- : "http://localhost:4000/fear/api/";
9
-
10
- const ACCESS_TOKEN_NAME = (process.env.JWT_TOKEN)
11
- ? process.env.JWT_TOKEN
12
- : "x-token";
13
-
14
- const instance = axios.create({
15
- baseURL: `${API_BASE_URL}`,
16
- headers: {
17
- Accept: "application/json",
18
- "Content-Type": "application/json",
19
- },
20
- paramsSerializer: (params) => {
21
- return qs.stringify(params, { indexes: false });
22
- },
23
- credentials: true
24
- //httpsAgent: new https.Agent({ rejectUnauthorized: false })
25
- });
26
-
27
- instance.interceptors.request.use(
28
- (config) => {
29
- const isAuth = cache.local.get("auth") ? cache.local.get("auth") : null;
30
- let token = isAuth !== null ? isAuth.token : "";
31
-
32
- config.headers = {
33
- Authorization: `Bearer ${token}`,
34
- [ACCESS_TOKEN_NAME]: token
35
- };
36
-
37
- return config;
38
- },
39
- (error) => { Promise.reject(error) }
40
- );
41
-
42
- instance.interceptors.response.use(
43
- (response) => {
44
- console.log("API RES :: ", response);
45
- const messages = response.data.message;
46
-
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
- cache.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);
66
- }
67
- );
68
-
69
-
70
- export const API = instance;
71
-
72
- export default API;
@@ -1,72 +0,0 @@
1
- import axios from "axios";
2
- import qs from "qs";
3
- import cache from "./cache";
4
-
5
-
6
- const API_BASE_URL = (process.env.NODE_ENV === "production")
7
- ? "http://fear.master.com:4000/fear/api/"
8
- : "http://localhost:4000/fear/api/";
9
-
10
- const ACCESS_TOKEN_NAME = (process.env.JWT_TOKEN)
11
- ? process.env.JWT_TOKEN
12
- : "x-token";
13
-
14
- const instance = axios.create({
15
- baseURL: `${API_BASE_URL}`,
16
- headers: {
17
- Accept: "application/json",
18
- "Content-Type": "application/json",
19
- },
20
- paramsSerializer: (params) => {
21
- return qs.stringify(params, { indexes: false });
22
- },
23
- credentials: true
24
- //httpsAgent: new https.Agent({ rejectUnauthorized: false })
25
- });
26
-
27
- instance.interceptors.request.use(
28
- (config) => {
29
- const isAuth = cache.local.get("auth") ? cache.local.get("auth") : null;
30
- let token = isAuth !== null ? isAuth.token : "";
31
-
32
- config.headers = {
33
- Authorization: `Bearer ${token}`,
34
- [ACCESS_TOKEN_NAME]: token
35
- };
36
-
37
- return config;
38
- },
39
- (error) => { Promise.reject(error) }
40
- );
41
-
42
- instance.interceptors.response.use(
43
- (response) => {
44
- console.log("API RES :: ", response);
45
- const messages = response.data.message;
46
-
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
- cache.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);
66
- }
67
- );
68
-
69
-
70
- export const API = instance;
71
-
72
- export default API;
@@ -1,71 +0,0 @@
1
-
2
-
3
- // cache
4
- export const cache = (options = {}) => {
5
- var engine = options.type == 'local' ? 'localStorage' : 'sessionStorage';
6
-
7
- return {
8
- check: () => {
9
- if (!window[engine]) {
10
- return false;
11
- }
12
- return true;
13
- },
14
- set: (key, value) => {
15
- if (!key) throw Error('Error:> Invalid key');
16
-
17
- try {
18
- window[engine].setItem(key, JSON.stringify(value));
19
-
20
- } catch (error) {
21
- console.error(`Error setting item ${key}:`, error);
22
- return false;
23
- }
24
- return true;
25
- },
26
- get: (key) => {
27
- try {
28
- if (key !== "undefined") {
29
- const data = window[engine].getItem(key);
30
- return data ? JSON.parse(data) : null;
31
- }
32
-
33
-
34
- } catch (error) {
35
- console.error(`Error getting item ${key}:`, error);
36
- return null;
37
- }
38
- },
39
- remove: function (key) {
40
- window[engine].removeItem(key);
41
- },
42
- clear: () => {
43
- window[engine].clear();
44
- },
45
- keys: () => {
46
- return Object.keys(window[engine]);
47
- },
48
- has: (key) => {
49
- return window[engine].getItem(key) !== null;
50
- },
51
- /*
52
- _extend: () => {
53
- const destination = typeof arguments[0] === 'object' ? arguments[0] : {};
54
-
55
- for (var i = 1; i < arguments.length; i++) {
56
- if (arguments[i] && typeof arguments[i] === 'object') {
57
- for (var property in arguments[i])
58
- destination[property] = arguments[i][property];
59
- }
60
- }
61
-
62
- return destination;
63
- }
64
- */
65
- };
66
- }
67
-
68
- cache.local = cache({ type: 'local' });
69
- cache.session = cache({ type: 'session' });
70
-
71
- export default cache;
@@ -1,71 +0,0 @@
1
-
2
-
3
- // cache
4
- export const cache = (options = {}) => {
5
- var engine = options.type == 'local' ? 'localStorage' : 'sessionStorage';
6
-
7
- return {
8
- check: () => {
9
- if (!window[engine]) {
10
- return false;
11
- }
12
- return true;
13
- },
14
- set: (key, value) => {
15
- if (!key) throw Error('Error:> Invalid key');
16
-
17
- try {
18
- window[engine].setItem(key, JSON.stringify(value));
19
-
20
- } catch (error) {
21
- console.error(`Error setting item ${key}:`, error);
22
- return false;
23
- }
24
- return true;
25
- },
26
- get: (key) => {
27
- try {
28
- if (key !== "undefined") {
29
- const data = window[engine].getItem(key);
30
- return data ? JSON.parse(data) : null;
31
- }
32
-
33
-
34
- } catch (error) {
35
- console.error(`Error getting item ${key}:`, error);
36
- return null;
37
- }
38
- },
39
- remove: function (key) {
40
- window[engine].removeItem(key);
41
- },
42
- clear: () => {
43
- window[engine].clear();
44
- },
45
- keys: () => {
46
- return Object.keys(window[engine]);
47
- },
48
- has: (key) => {
49
- return window[engine].getItem(key) !== null;
50
- },
51
- /*
52
- _extend: () => {
53
- const destination = typeof arguments[0] === 'object' ? arguments[0] : {};
54
-
55
- for (var i = 1; i < arguments.length; i++) {
56
- if (arguments[i] && typeof arguments[i] === 'object') {
57
- for (var property in arguments[i])
58
- destination[property] = arguments[i][property];
59
- }
60
- }
61
-
62
- return destination;
63
- }
64
- */
65
- };
66
- }
67
-
68
- cache.local = cache({ type: 'local' });
69
- cache.session = cache({ type: 'session' });
70
-
71
- export default cache;