@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,115 +1,186 @@
1
1
  import { createSlice, createEntityAdapter, combineReducers } from '@reduxjs/toolkit';
2
2
  import ThunkFactory from './thunk';
3
- import StateFactory from "./state";
4
- //import ApiFactory from './service';
5
-
3
+ import StateFactory from './state';
6
4
 
5
+ /**
6
+ * Creates a Redux feature with standardized structure including slice, async actions, and reducers
7
+ * @param {string} entity - The entity name for the feature
8
+ * @param {Object} reducers - Custom reducers to include in the slice
9
+ * @param {Object|null} endpoints - API endpoints (currently unused)
10
+ * @returns {Object} Feature factory instance with methods to create slices and manage reducers
11
+ */
7
12
  export function FeatureFactory(entity, reducers = {}, endpoints = null) {
13
+ if (!entity || typeof entity !== 'string') {
14
+ throw new Error('Entity name must be provided as a string');
15
+ }
8
16
 
9
- const _this = {
10
- entity,
11
- reducers,
12
- //api: ApiFactory,
13
- thunk: ThunkFactory,
14
- state: StateFactory,
15
- adapter: createEntityAdapter(),
16
-
17
- manager: (initialReducers) => {
18
- const reducers = { ...initialReducers };
19
- let combinedReducer = combineReducers(reducers);
17
+ const adapter = createEntityAdapter();
20
18
 
21
- return {
22
- reduce: (state, action) => combinedReducer(state, action),
23
- add: (key, reducer) => {
24
- if (!key || reducers[key]) return;
25
- reducers[key] = reducer;
26
- combinedReducer = combineReducers(reducers);
27
- },
28
- remove: (key) => {
29
- if (!key || !reducers[key]) return;
30
- delete reducers[key];
31
- combinedReducer = combineReducers(reducers);
32
- },
33
- getReducerMap: () => reducers,
34
- };
35
- },
36
- inject: (source, dest) => {
37
- for (var prop in source) {
38
- if (source.hasOwnProperty(prop)) {
39
- dest[prop] = source[prop];
40
- }
19
+ /**
20
+ * Creates a dynamic reducer manager for adding/removing reducers at runtime
21
+ * @param {Object} initialReducers - Initial set of reducers
22
+ * @returns {Object} Reducer manager with methods to manipulate reducers
23
+ */
24
+ const createReducerManager = (initialReducers = {}) => {
25
+ const reducersMap = { ...initialReducers };
26
+ let combinedReducer = combineReducers(reducersMap);
27
+
28
+ return {
29
+ reduce: (state, action) => combinedReducer(state, action),
30
+
31
+ add: (key, reducer) => {
32
+ if (!key || typeof key !== 'string') {
33
+ console.warn('Invalid reducer key provided');
34
+ return;
41
35
  }
42
- return dest;
43
- },
44
- create: (options = { service: null, initialState: null }) => {
45
- const { service, initialState } = options;
46
- const sliceName = _this.entity;
47
- const standard = {
48
- fetch: _this.thunk.create(sliceName, 'all'),
49
- fetchOne: _this.thunk.create(sliceName, 'one'),
50
- search: _this.thunk.create(sliceName, 'search')
36
+ if (reducersMap[key]) {
37
+ console.warn(`Reducer with key '${key}' already exists`);
38
+ return;
51
39
  }
40
+
41
+ reducersMap[key] = reducer;
42
+ combinedReducer = combineReducers(reducersMap);
43
+ },
44
+
45
+ remove: (key) => {
46
+ if (!key || !reducersMap[key]) {
47
+ console.warn(`Reducer with key '${key}' does not exist`);
48
+ return;
49
+ }
50
+
51
+ delete reducersMap[key];
52
+ combinedReducer = combineReducers(reducersMap);
53
+ },
54
+
55
+ getReducerMap: () => ({ ...reducersMap }),
56
+ };
57
+ };
52
58
 
53
- const factorySlice = createSlice({
54
- name: sliceName,
55
- initialState: StateFactory(sliceName),
56
- reducers: _this.reducers,
57
- extraReducers: (builder) => {
58
- Object.keys(standard).forEach(key => {
59
- builder
60
- .addCase(standard[key].pending, (state) => {
61
- state.loading = true;
62
- state.error = null;
63
- })
64
- .addCase(standard[key].fulfilled, (state, action) => {
65
- state.loading = false;
66
- state.success = true;
67
- state.data = action.payload;
68
- state[sliceName] = action.payload[0];
69
- if (key == 'fetchOne') {
70
- state[sliceName] = action.payload[0];
71
- }
72
- })
73
- .addCase(standard[key].rejected, (state, action) => {
74
- state.loading = false;
75
- state.success = false;
76
- state.error = action.error;
77
- });
78
- })
79
- if (service) {
80
- Object.keys(service).forEach(key => {
81
- if (service[key] != standard[key]) {
82
- builder
83
- .addCase(service[key].pending, (state) => {
84
- state.loading = true;
85
- state.error = null;
86
- })
87
- .addCase(service[key].fulfilled, (state, action) => {
88
- state.loading = false;
89
- state.success = true;
90
- state.data = action.payload;
91
- state[sliceName] = action.payload[0]
92
- })
93
- .addCase(service[key].rejected, (state, action) => {
94
- state.loading = false;
95
- state.success = false;
96
- state.error = action.payload;
97
- });
98
- }
99
- })
100
- }}
101
- }
102
- )
59
+ /**
60
+ * Merges properties from source object into destination object
61
+ * @param {Object} source - Source object
62
+ * @param {Object} destination - Destination object
63
+ * @returns {Object} Merged destination object
64
+ */
65
+ const mergeObjects = (source, destination) => {
66
+ if (!source || !destination) {
67
+ throw new Error('Both source and destination objects must be provided');
68
+ }
103
69
 
104
- const asyncActions = _this.inject(standard, (service) ? service : {});
70
+ return Object.keys(source).reduce((acc, key) => {
71
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
72
+ acc[key] = source[key];
73
+ }
74
+ return acc;
75
+ }, { ...destination });
76
+ };
105
77
 
106
- return {
107
- slice: factorySlice,
108
- asyncActions
109
- };
110
- }}
78
+ /**
79
+ * Creates standard async thunks for common operations
80
+ * @param {string} sliceName - Name of the slice
81
+ * @returns {Object} Standard async thunks
82
+ */
83
+ const createStandardThunks = (sliceName) => ({
84
+ fetch: ThunkFactory.create(sliceName, 'all'),
85
+ fetchOne: ThunkFactory.create(sliceName, 'one'),
86
+ search: ThunkFactory.create(sliceName, 'search'),
87
+ });
111
88
 
112
- return _this;
89
+ /**
90
+ * Adds standard async action handlers to builder
91
+ * @param {Object} builder - RTK builder object
92
+ * @param {Object} asyncActions - Async actions to handle
93
+ * @param {string} sliceName - Name of the slice
94
+ */
95
+ const addAsyncActionHandlers = (builder, asyncActions, sliceName) => {
96
+ Object.entries(asyncActions).forEach(([key, action]) => {
97
+ builder
98
+ .addCase(action.pending, (state) => {
99
+ state.loading = true;
100
+ state.error = null;
101
+ })
102
+ .addCase(action.fulfilled, (state, actionPayload) => {
103
+ state.loading = false;
104
+ state.success = true;
105
+ state.data = actionPayload.payload;
106
+
107
+ // Handle single item for fetchOne operation
108
+ if (key === 'fetchOne' && Array.isArray(actionPayload.payload)) {
109
+ state[sliceName] = actionPayload.payload[0];
110
+ } else if (Array.isArray(actionPayload.payload) && actionPayload.payload.length > 0) {
111
+ state[sliceName] = actionPayload.payload[0];
112
+ }
113
+ })
114
+ .addCase(action.rejected, (state, actionPayload) => {
115
+ state.loading = false;
116
+ state.success = false;
117
+ state.error = actionPayload.error || actionPayload.payload;
118
+ });
119
+ });
120
+ };
121
+
122
+ /**
123
+ * Creates a Redux slice with standard and custom async actions
124
+ * @param {Object} options - Configuration options
125
+ * @param {Object} options.service - Custom service actions
126
+ * @param {Object} options.initialState - Custom initial state
127
+ * @returns {Object} Created slice and async actions
128
+ */
129
+ const createFactorySlice = (options = {}) => {
130
+ const { service = null, initialState = null } = options;
131
+ const sliceName = entity;
132
+
133
+ // Create standard thunks
134
+ const standardThunks = createStandardThunks(sliceName);
135
+
136
+ // Create the slice
137
+ const factorySlice = createSlice({
138
+ name: sliceName,
139
+ initialState: initialState || StateFactory(sliceName),
140
+ reducers,
141
+ extraReducers: (builder) => {
142
+ // Add standard async action handlers
143
+ addAsyncActionHandlers(builder, standardThunks, sliceName);
144
+
145
+ // Add custom service action handlers
146
+ if (service && typeof service === 'object') {
147
+ const customActions = Object.entries(service).reduce((acc, [key, action]) => {
148
+ // Only add if it's not already in standard thunks
149
+ if (!standardThunks[key]) {
150
+ acc[key] = action;
151
+ }
152
+ return acc;
153
+ }, {});
154
+
155
+ addAsyncActionHandlers(builder, customActions, sliceName);
156
+ }
157
+ },
158
+ });
159
+
160
+ // Merge standard and custom async actions
161
+ const asyncActions = service
162
+ ? mergeObjects(standardThunks, service)
163
+ : standardThunks;
164
+
165
+ return {
166
+ slice: factorySlice,
167
+ asyncActions,
168
+ };
169
+ };
170
+
171
+ // Public API
172
+ return {
173
+ entity,
174
+ reducers,
175
+ adapter,
176
+ manager: createReducerManager,
177
+ inject: mergeObjects,
178
+ create: createFactorySlice,
179
+
180
+ // Utility methods
181
+ getEntityName: () => entity,
182
+ getAdapter: () => adapter,
183
+ };
113
184
  }
114
185
 
115
186
  export default FeatureFactory;