@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,9 +1,434 @@
1
- export const StateFactory = (namespace) => ({
2
- [namespace]: {},
1
+ /**
2
+ * Default state configuration options
3
+ */
4
+ const DEFAULT_OPTIONS = {
5
+ includeEntityState: true,
6
+ includePagination: true,
7
+ includeFiltering: true,
8
+ includeSorting: true,
9
+ includeSelection: true,
10
+ includeValidation: false,
11
+ includeMetadata: true,
12
+ customFields: {},
13
+ };
14
+
15
+ /**
16
+ * Standard loading states for async operations
17
+ */
18
+ const LOADING_STATES = {
19
+ IDLE: 'idle',
20
+ PENDING: 'pending',
21
+ FULFILLED: 'fulfilled',
22
+ REJECTED: 'rejected',
23
+ };
24
+
25
+ /**
26
+ * Standard sort orders
27
+ */
28
+ const SORT_ORDERS = {
29
+ ASC: 'asc',
30
+ DESC: 'desc',
31
+ };
32
+
33
+ /**
34
+ * Validates the namespace parameter
35
+ * @param {string} namespace - The namespace for the state
36
+ * @throws {Error} If namespace is invalid
37
+ */
38
+ const validateNamespace = (namespace) => {
39
+ if (!namespace || typeof namespace !== 'string') {
40
+ throw new Error('Namespace must be a non-empty string');
41
+ }
42
+ if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(namespace)) {
43
+ throw new Error('Namespace must be a valid identifier (letters, numbers, underscore, starting with letter)');
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Creates pagination state
49
+ * @returns {Object} Pagination state object
50
+ */
51
+ const createPaginationState = () => ({
52
+ currentPage: 1,
53
+ pageSize: 10,
54
+ totalItems: 0,
55
+ totalPages: 0,
56
+ hasNextPage: false,
57
+ hasPreviousPage: false,
58
+ });
59
+
60
+ /**
61
+ * Creates filtering state
62
+ * @returns {Object} Filtering state object
63
+ */
64
+ const createFilteringState = () => ({
65
+ filters: {},
66
+ activeFilters: [],
67
+ searchTerm: '',
68
+ searchFields: [],
69
+ });
70
+
71
+ /**
72
+ * Creates sorting state
73
+ * @returns {Object} Sorting state object
74
+ */
75
+ const createSortingState = () => ({
76
+ sortBy: null,
77
+ sortOrder: SORT_ORDERS.ASC,
78
+ multiSort: [],
79
+ });
80
+
81
+ /**
82
+ * Creates selection state
83
+ * @returns {Object} Selection state object
84
+ */
85
+ const createSelectionState = () => ({
86
+ selectedItems: [],
87
+ selectedIds: [],
88
+ allSelected: false,
89
+ selectionMode: 'multiple', // 'single', 'multiple', 'none'
90
+ });
91
+
92
+ /**
93
+ * Creates validation state
94
+ * @returns {Object} Validation state object
95
+ */
96
+ const createValidationState = () => ({
97
+ validationErrors: {},
98
+ isValid: true,
99
+ fieldErrors: {},
100
+ touched: {},
101
+ });
102
+
103
+ /**
104
+ * Creates metadata state for tracking additional information
105
+ * @returns {Object} Metadata state object
106
+ */
107
+ const createMetadataState = () => ({
108
+ lastFetch: null,
109
+ lastUpdate: null,
110
+ version: 0,
111
+ source: null,
112
+ cached: false,
113
+ stale: false,
114
+ });
115
+
116
+ /**
117
+ * Creates async operation state for tracking multiple operations
118
+ * @returns {Object} Async operations state
119
+ */
120
+ const createAsyncOperationsState = () => ({
121
+ operations: {},
122
+ globalLoading: false,
123
+ operationQueue: [],
124
+ });
125
+
126
+ /**
127
+ * Deep merges two objects
128
+ * @param {Object} target - Target object
129
+ * @param {Object} source - Source object
130
+ * @returns {Object} Merged object
131
+ */
132
+ const deepMerge = (target, source) => {
133
+ if (!source || typeof source !== 'object') return target;
134
+
135
+ const result = { ...target };
136
+
137
+ Object.keys(source).forEach(key => {
138
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
139
+ result[key] = deepMerge(target[key] || {}, source[key]);
140
+ } else {
141
+ result[key] = source[key];
142
+ }
143
+ });
144
+
145
+ return result;
146
+ };
147
+
148
+ /**
149
+ * Creates a comprehensive Redux state structure for an entity
150
+ * @param {string} namespace - The entity namespace/name
151
+ * @param {Object} options - Configuration options for state creation
152
+ * @param {boolean} options.includeEntityState - Include entity-specific state
153
+ * @param {boolean} options.includePagination - Include pagination state
154
+ * @param {boolean} options.includeFiltering - Include filtering state
155
+ * @param {boolean} options.includeSorting - Include sorting state
156
+ * @param {boolean} options.includeSelection - Include selection state
157
+ * @param {boolean} options.includeValidation - Include validation state
158
+ * @param {boolean} options.includeMetadata - Include metadata state
159
+ * @param {Object} options.customFields - Custom fields to add to state
160
+ * @returns {Object} Complete initial state object
161
+ */
162
+ export const StateFactory = (namespace, options = {}) => {
163
+ validateNamespace(namespace);
164
+
165
+ const config = { ...DEFAULT_OPTIONS, ...options };
166
+
167
+ // Base state - always included
168
+ const baseState = {
169
+ // Core data
3
170
  data: [],
4
171
  loading: false,
5
172
  success: false,
6
- error: null
7
- });
173
+ error: null,
174
+
175
+ // Enhanced loading states
176
+ loadingState: LOADING_STATES.IDLE,
177
+
178
+ // Entity-specific state (dynamic key)
179
+ ...(config.includeEntityState && {
180
+ [namespace]: null,
181
+ [`${namespace}List`]: [],
182
+ [`current${namespace.charAt(0).toUpperCase() + namespace.slice(1)}`]: null,
183
+ }),
184
+ };
185
+
186
+ // Optional state sections
187
+ const optionalStates = {
188
+ ...(config.includePagination && {
189
+ pagination: createPaginationState(),
190
+ }),
191
+
192
+ ...(config.includeFiltering && {
193
+ filtering: createFilteringState(),
194
+ }),
195
+
196
+ ...(config.includeSorting && {
197
+ sorting: createSortingState(),
198
+ }),
199
+
200
+ ...(config.includeSelection && {
201
+ selection: createSelectionState(),
202
+ }),
203
+
204
+ ...(config.includeValidation && {
205
+ validation: createValidationState(),
206
+ }),
207
+
208
+ ...(config.includeMetadata && {
209
+ metadata: createMetadataState(),
210
+ }),
211
+
212
+ // Async operations tracking
213
+ async: createAsyncOperationsState(),
214
+ };
215
+
216
+ // Merge all states
217
+ const initialState = {
218
+ ...baseState,
219
+ ...optionalStates,
220
+ ...config.customFields,
221
+ };
222
+
223
+ return initialState;
224
+ };
225
+
226
+ /**
227
+ * Creates a minimal state for simple use cases
228
+ * @param {string} namespace - The entity namespace
229
+ * @returns {Object} Minimal state object
230
+ */
231
+ StateFactory.minimal = (namespace) => {
232
+ return StateFactory(namespace, {
233
+ includeEntityState: true,
234
+ includePagination: false,
235
+ includeFiltering: false,
236
+ includeSorting: false,
237
+ includeSelection: false,
238
+ includeValidation: false,
239
+ includeMetadata: false,
240
+ });
241
+ };
242
+
243
+ /**
244
+ * Creates state optimized for lists/tables
245
+ * @param {string} namespace - The entity namespace
246
+ * @returns {Object} List-optimized state object
247
+ */
248
+ StateFactory.forList = (namespace) => {
249
+ return StateFactory(namespace, {
250
+ includeEntityState: true,
251
+ includePagination: true,
252
+ includeFiltering: true,
253
+ includeSorting: true,
254
+ includeSelection: true,
255
+ includeValidation: false,
256
+ includeMetadata: true,
257
+ });
258
+ };
259
+
260
+ /**
261
+ * Creates state optimized for forms
262
+ * @param {string} namespace - The entity namespace
263
+ * @returns {Object} Form-optimized state object
264
+ */
265
+ StateFactory.forForm = (namespace) => {
266
+ return StateFactory(namespace, {
267
+ includeEntityState: true,
268
+ includePagination: false,
269
+ includeFiltering: false,
270
+ includeSorting: false,
271
+ includeSelection: false,
272
+ includeValidation: true,
273
+ includeMetadata: true,
274
+ customFields: {
275
+ formData: {},
276
+ isDirty: false,
277
+ isSubmitting: false,
278
+ submitCount: 0,
279
+ },
280
+ });
281
+ };
282
+
283
+ /**
284
+ * Creates state for real-time/live data
285
+ * @param {string} namespace - The entity namespace
286
+ * @returns {Object} Real-time optimized state object
287
+ */
288
+ StateFactory.forRealTime = (namespace) => {
289
+ return StateFactory(namespace, {
290
+ includeEntityState: true,
291
+ includePagination: false,
292
+ includeFiltering: false,
293
+ includeSorting: false,
294
+ includeSelection: false,
295
+ includeValidation: false,
296
+ includeMetadata: true,
297
+ customFields: {
298
+ connected: false,
299
+ connectionStatus: 'disconnected',
300
+ lastHeartbeat: null,
301
+ subscriptions: [],
302
+ liveUpdates: true,
303
+ },
304
+ });
305
+ };
306
+
307
+ /**
308
+ * State factory for normalized entities (works well with RTK's createEntityAdapter)
309
+ * @param {string} namespace - The entity namespace
310
+ * @returns {Object} Normalized state structure
311
+ */
312
+ StateFactory.normalized = (namespace) => {
313
+ return StateFactory(namespace, {
314
+ includeEntityState: false,
315
+ customFields: {
316
+ ids: [],
317
+ entities: {},
318
+ },
319
+ });
320
+ };
321
+
322
+ /**
323
+ * Creates state with custom async operations tracking
324
+ * @param {string} namespace - The entity namespace
325
+ * @param {string[]} operations - List of operation names to track
326
+ * @returns {Object} State with operation tracking
327
+ */
328
+ StateFactory.withOperations = (namespace, operations = []) => {
329
+ const operationStates = operations.reduce((acc, op) => {
330
+ acc[`${op}Loading`] = false;
331
+ acc[`${op}Success`] = false;
332
+ acc[`${op}Error`] = null;
333
+ return acc;
334
+ }, {});
335
+
336
+ return StateFactory(namespace, {
337
+ customFields: {
338
+ ...operationStates,
339
+ operations: operations.reduce((acc, op) => {
340
+ acc[op] = {
341
+ loading: false,
342
+ success: false,
343
+ error: null,
344
+ lastRun: null,
345
+ };
346
+ return acc;
347
+ }, {}),
348
+ },
349
+ });
350
+ };
351
+
352
+ /**
353
+ * Utility functions for working with state
354
+ */
355
+ StateFactory.utils = {
356
+ /**
357
+ * Gets loading states enum
358
+ * @returns {Object} Loading states
359
+ */
360
+ getLoadingStates: () => ({ ...LOADING_STATES }),
361
+
362
+ /**
363
+ * Gets sort orders enum
364
+ * @returns {Object} Sort orders
365
+ */
366
+ getSortOrders: () => ({ ...SORT_ORDERS }),
367
+
368
+ /**
369
+ * Creates a loading state updater
370
+ * @param {string} operation - Operation name
371
+ * @returns {Object} Loading state updates
372
+ */
373
+ createLoadingUpdates: (operation = 'default') => ({
374
+ pending: {
375
+ loading: true,
376
+ success: false,
377
+ error: null,
378
+ loadingState: LOADING_STATES.PENDING,
379
+ [`async.operations.${operation}.loading`]: true,
380
+ },
381
+ fulfilled: {
382
+ loading: false,
383
+ success: true,
384
+ error: null,
385
+ loadingState: LOADING_STATES.FULFILLED,
386
+ [`async.operations.${operation}.loading`]: false,
387
+ [`async.operations.${operation}.success`]: true,
388
+ [`async.operations.${operation}.lastRun`]: new Date().toISOString(),
389
+ },
390
+ rejected: (error) => ({
391
+ loading: false,
392
+ success: false,
393
+ error,
394
+ loadingState: LOADING_STATES.REJECTED,
395
+ [`async.operations.${operation}.loading`]: false,
396
+ [`async.operations.${operation}.error`]: error,
397
+ [`async.operations.${operation}.lastRun`]: new Date().toISOString(),
398
+ }),
399
+ }),
400
+
401
+ /**
402
+ * Validates state structure
403
+ * @param {Object} state - State to validate
404
+ * @param {string} namespace - Expected namespace
405
+ * @returns {boolean} Whether state is valid
406
+ */
407
+ validateState: (state, namespace) => {
408
+ if (!state || typeof state !== 'object') return false;
409
+ if (!Array.isArray(state.data)) return false;
410
+ if (typeof state.loading !== 'boolean') return false;
411
+ if (typeof state.success !== 'boolean') return false;
412
+ return true;
413
+ },
414
+
415
+ /**
416
+ * Deep merges state updates
417
+ * @param {Object} currentState - Current state
418
+ * @param {Object} updates - State updates
419
+ * @returns {Object} Merged state
420
+ */
421
+ mergeState: deepMerge,
422
+
423
+ /**
424
+ * Resets state to initial values
425
+ * @param {string} namespace - Entity namespace
426
+ * @param {Object} options - State options
427
+ * @returns {Object} Reset state
428
+ */
429
+ resetState: (namespace, options = {}) => {
430
+ return StateFactory(namespace, options);
431
+ },
432
+ };
8
433
 
9
- export default StateFactory;
434
+ export default StateFactory;