@feardread/feature-factory 2.0.5 → 3.0.1

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,23 +1,27 @@
1
1
  {
2
2
  "name": "@feardread/feature-factory",
3
- "version": "2.0.5",
3
+ "version": "3.0.1",
4
+ "description": "Library to interact with redux toolkit and reduce boilerplate / repeated code",
4
5
  "main": "dist/index.js",
5
6
  "scripts": {
6
- "build": "rollup -c",
7
+ "build": "npx rollup -c ",
7
8
  "test": "echo \"Error: no test specified\" && exit 1"
8
9
  },
9
- "author": "Garrett Haptonstall (FearDread)",
10
+ "keywords": [
11
+ "ecommerce",
12
+ "features",
13
+ "redux",
14
+ "toolkit"
15
+ ],
16
+ "author": "FearDread",
10
17
  "license": "MIT",
11
18
  "dependencies": {
12
- "@reduxjs/toolkit": "^2.2.7",
13
- "@rollup/plugin-terser": "^0.4.4",
14
- "axios": "^1.7.7",
15
- "react": "^18.3.1",
16
- "react-dom": "^18.3.1",
17
- "react-redux": "^9.1.2",
18
- "react-toastify": "^10.0.5",
19
- "rollup-plugin-peer-deps-external": "^2.2.4",
20
- "rollup-plugin-polyfill-node": "^0.13.0"
19
+ "@babel/core": "^7.28.0",
20
+ "@reduxjs/toolkit": "^2.8.2",
21
+ "axios": "^1.11.0",
22
+ "react": "^19.1.0",
23
+ "react-redux": "^9.2.0",
24
+ "typescript": "^5.8.3"
21
25
  },
22
26
  "devDependencies": {
23
27
  "@babel/preset-env": "^7.25.4",
@@ -26,8 +30,22 @@
26
30
  "@rollup/plugin-commonjs": "^28.0.0",
27
31
  "@rollup/plugin-json": "^6.1.0",
28
32
  "@rollup/plugin-node-resolve": "^15.3.0",
33
+ "@rollup/plugin-terser": "^0.4.4",
29
34
  "rollup": "^4.22.4",
30
- "rollup-plugin-postcss": "^4.0.2"
35
+ "rollup-plugin-dts": "^6.1.1",
36
+ "rollup-plugin-peer-deps-external": "^2.2.4",
37
+ "rollup-plugin-polyfill-node": "^0.13.0"
31
38
  },
32
- "description": ""
39
+ "browserslist": {
40
+ "production": [
41
+ ">0.2%",
42
+ "not dead",
43
+ "not op_mini all"
44
+ ],
45
+ "development": [
46
+ "last 1 chrome version",
47
+ "last 1 firefox version",
48
+ "last 1 safari version"
49
+ ]
50
+ }
33
51
  }
package/rollup.config.mjs CHANGED
@@ -4,11 +4,30 @@ import commonjs from "@rollup/plugin-commonjs";
4
4
  import json from "@rollup/plugin-json";
5
5
  import polyfill from "rollup-plugin-polyfill-node";
6
6
  import peerDepsExternal from 'rollup-plugin-peer-deps-external';
7
-
7
+ import typescript from '@rollup/plugin-typescript'
8
8
  import pkg from "./package.json" assert { type: 'json' };
9
+ import dts from 'rollup-plugin-dts'
10
+ const config = [
11
+ {
12
+ input: 'src/index.js',
13
+ output: {
14
+ file: 'refactory.js',
15
+ format: 'cjs',
16
+ sourcemap: true,
17
+ },
18
+ external: ['axios', 'os', 'url'],
19
+ plugins: [typescript()]
20
+ }, {
21
+ input: 'src/index.d.ts',
22
+ output: {
23
+ file: 'refactory.d.ts',
24
+ format: 'es'
25
+ },
26
+ plugins: [dts()]
27
+ }
28
+ ];
9
29
 
10
- export default [
11
- {
30
+ const jsconfig = [{
12
31
  input: 'src/index.js',
13
32
  output: [
14
33
  {
@@ -41,4 +60,44 @@ export default [
41
60
  terser()
42
61
  ]
43
62
  },
44
- ];
63
+ ];
64
+
65
+ export default jsconfig;
66
+ /*
67
+ export default [
68
+ {
69
+ input: 'src/index.ts',
70
+ output: [
71
+ {
72
+ file: 'dist/index.js',
73
+ format: 'cjs',
74
+ exports: 'named',
75
+ sourcemap: true,
76
+ },
77
+ {
78
+ file: 'dist/index.esm.js',
79
+ format: "esm",
80
+ exports: 'named',
81
+ sourcemap: true,
82
+ },
83
+ {
84
+ file: 'dist/bundle.min.js',
85
+ format: 'iife',
86
+ name: 'version',
87
+ plugins: [terser()]
88
+ }
89
+ ],
90
+ plugins: [
91
+ peerDepsExternal(),
92
+ resolve({
93
+ browser: true,
94
+ preferBuiltins: false,
95
+ }),
96
+ commonjs(),
97
+ json(),
98
+ terser(),
99
+ typescript()
100
+ ]
101
+ },
102
+ ];
103
+ */
@@ -0,0 +1,72 @@
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;
@@ -0,0 +1,72 @@
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,9 +1,9 @@
1
1
 
2
2
 
3
3
  // cache
4
- const cache = (options = {}) => {
4
+ export const cache = (options = {}) => {
5
5
  var engine = options.type == 'local' ? 'localStorage' : 'sessionStorage';
6
-
6
+
7
7
  return {
8
8
  check: () => {
9
9
  if (!window[engine]) {
@@ -25,9 +25,9 @@ const cache = (options = {}) => {
25
25
  },
26
26
  get: (key) => {
27
27
  try {
28
- if ( key !== "undefined") {
29
- const data = window[engine].getItem(key);
30
- return data ? JSON.parse(data) : null;
28
+ if (key !== "undefined") {
29
+ const data = window[engine].getItem(key);
30
+ return data ? JSON.parse(data) : null;
31
31
  }
32
32
 
33
33
 
@@ -65,7 +65,7 @@ const cache = (options = {}) => {
65
65
  };
66
66
  }
67
67
 
68
- cache.local = cache({type: 'local'});
69
- cache.session = cache({type: 'session'});
68
+ cache.local = cache({ type: 'local' });
69
+ cache.session = cache({ type: 'session' });
70
70
 
71
71
  export default cache;
@@ -0,0 +1,71 @@
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;
@@ -0,0 +1,149 @@
1
+ import { createSlice, createEntityAdapter, combineReducers } from '@reduxjs/toolkit';
2
+ import ThunkFactory from './thunk';
3
+ //import ApiFactory from './service';
4
+
5
+ export const StateFactory = (namespace) => ({
6
+ [namespace]: {},
7
+ data: [],
8
+ loading: false,
9
+ success: false,
10
+ error: null
11
+ });
12
+
13
+ export function FeatureFactory(entity, reducers = {}, endpoints = null) {
14
+
15
+ const factory = {
16
+ entity,
17
+ reducers,
18
+ //api: ApiFactory,
19
+ thunk: ThunkFactory,
20
+ state: StateFactory,
21
+ adapter: createEntityAdapter()
22
+ };
23
+
24
+ factory.manager = (initialReducers) => {
25
+ const reducers = { ...initialReducers };
26
+ let combinedReducer = combineReducers(reducers);
27
+
28
+ return {
29
+ reduce: (state, action) => combinedReducer(state, action),
30
+ add: (key, reducer) => {
31
+ if (!key || reducers[key]) return;
32
+ reducers[key] = reducer;
33
+ combinedReducer = combineReducers(reducers);
34
+ },
35
+ remove: (key) => {
36
+ if (!key || !reducers[key]) return;
37
+ delete reducers[key];
38
+ combinedReducer = combineReducers(reducers);
39
+ },
40
+ getReducerMap: () => reducers,
41
+ };
42
+ }
43
+
44
+ factory.inject = (source, dest) => {
45
+ for (var prop in source) {
46
+ if (source.hasOwnProperty(prop)) {
47
+ dest[prop] = source[prop];
48
+ }
49
+ }
50
+ return dest;
51
+ }
52
+
53
+ factory.create = (options = { service: null, initialState: null }) => {
54
+ const { service, initialState } = options;
55
+ const sliceName = factory.entity;
56
+ const standard = {
57
+ fetch: factory.thunk.create(sliceName, 'all'),
58
+ fetchOne: factory.thunk.create(sliceName, 'one'),
59
+ search: factory.thunk.create(sliceName, 'search')
60
+ }
61
+
62
+ const factorySlice = createSlice({
63
+ name: sliceName,
64
+ initialState: StateFactory(sliceName),
65
+ reducers: factory.reducers,
66
+ extraReducers: (builder) => {
67
+ Object.key(standard).forEach( key => {
68
+ builder
69
+ .addCase(standard[key].pending, (state) => {
70
+ state.loading = true;
71
+ state.error = null;
72
+ })
73
+ .addCase(standard[key].fulfilled, (state, action) => {
74
+ state.loading = false;
75
+ state.success = true;
76
+ state.data = action.payload;
77
+ state[sliceName] = action.payload[0];
78
+ if (act == 'fetchOne') {
79
+ state[sliceName] = action.payload[0];
80
+ }
81
+ })
82
+ .addCase(standard[key].rejected, (state, action) => {
83
+ state.loading = false;
84
+ state.success = false;
85
+ state.error = action.error;
86
+ });
87
+ })
88
+ /*
89
+ for (var act in standard) {
90
+ if (standard.hasOwnProperty(act)) {
91
+ builder
92
+ .addCase(standard[act].pending, (state) => {
93
+ state.loading = true;
94
+ state.error = null;
95
+ })
96
+ .addCase(standard[act].fulfilled, (state, action) => {
97
+ state.loading = false;
98
+ state.success = true;
99
+ state.data = action.payload;
100
+ state[sliceName] = action.payload[0];
101
+ if (act == 'fetchOne') {
102
+ state[sliceName] = action.payload[0];
103
+ }
104
+ })
105
+ .addCase(standard[act].rejected, (state, action) => {
106
+ state.loading = false;
107
+ state.success = false;
108
+ state.error = action.error;
109
+ });
110
+ }
111
+ }
112
+ */
113
+ if (service) {
114
+ for (var key in service) {
115
+ if (service.hasOwnProperty(key) && !standard.hasOwnProperty(key)) {
116
+ builder
117
+ .addCase(service[key].pending, (state) => {
118
+ state.loading = true;
119
+ state.error = null;
120
+ })
121
+ .addCase(service[key].fulfilled, (state, action) => {
122
+ state.loading = false;
123
+ state.success = true;
124
+ state.data = action.payload;
125
+ console.log('action fulfilled :', state.data);
126
+ })
127
+ .addCase(service[key].rejected, (state, action) => {
128
+ state.loading = false;
129
+ state.success = false;
130
+ state.error = action.payload;
131
+ });
132
+ }
133
+ }
134
+
135
+ }
136
+ }
137
+ });
138
+
139
+ const asyncActions = factory.inject(standard, (service) ? service : {});
140
+ console.log('async = ', asyncActions);
141
+ return {
142
+ slice: factorySlice,
143
+ asyncActions
144
+ };
145
+ }
146
+ return factory;
147
+ }
148
+
149
+ export default FeatureFactory;
@@ -0,0 +1,158 @@
1
+ import {
2
+ createSlice,
3
+ createEntityAdapter,
4
+ combineReducers,
5
+ EntityAdapter,
6
+ AsyncThunk,
7
+ Reducer,
8
+ AnyAction,
9
+ SliceCaseReducers,
10
+ PayloadAction,
11
+ } from '@reduxjs/toolkit';
12
+ import ThunkFactory from './thunk';
13
+
14
+ // Define the shape of your feature state
15
+ export interface FeatureState<T> {
16
+ loading: boolean;
17
+ success: boolean;
18
+ error: unknown | null;
19
+ data: T[];
20
+ [key: string]: any;
21
+ }
22
+
23
+ // A helper type for mapping service thunks
24
+ export type ServiceMap = Record<string, AsyncThunk<any, any, any>>;
25
+
26
+ // Manager API for dynamic reducer injection
27
+ export interface ReducerManager {
28
+ reduce: (state: any, action: AnyAction) => any;
29
+ add: (key: string, reducer: Reducer<any, AnyAction>) => void;
30
+ remove: (key: string) => void;
31
+ getReducerMap: () => Record<string, Reducer<any, AnyAction>>;
32
+ }
33
+
34
+ // Assume you have a StateFactory function somewhere:
35
+ function StateFactory<T>(name: string): FeatureState<T> {
36
+ return {
37
+ loading: false,
38
+ success: false,
39
+ error: null,
40
+ data: [],
41
+ [name]: null,
42
+ };
43
+ }
44
+
45
+ // The main factory, now generic in T
46
+ export function FeatureFactory<T>(
47
+ entity: string,
48
+ reducers: SliceCaseReducers<FeatureState<T>> = {}, // → typed slices
49
+ endpoints: ServiceMap | null = null // → typed services
50
+ ) {
51
+ const factory = {
52
+ entity,
53
+ reducers,
54
+ manager: ReducerManager ,
55
+ thunk: ThunkFactory,
56
+ state: StateFactory as (name: string) => FeatureState<T>,
57
+ adapter: createEntityAdapter<T>() as EntityAdapter<T>, // → typed adapter
58
+ };
59
+
60
+ factory.manager = (initialReducers: Record<string, Reducer<any, AnyAction>> = {}) => {
61
+ const reducersMap = { ...initialReducers };
62
+ let combined = combineReducers(reducersMap);
63
+
64
+ const manager: ReducerManager = {
65
+ reduce: (state, action) => combined(state, action),
66
+ add: (key, reducer) => {
67
+ if (!key || reducersMap[key]) return;
68
+ reducersMap[key] = reducer;
69
+ combined = combineReducers(reducersMap);
70
+ },
71
+ remove: (key) => {
72
+ if (!key || !reducersMap[key]) return;
73
+ delete reducersMap[key];
74
+ combined = combineReducers(reducersMap);
75
+ },
76
+ getReducerMap: () => reducersMap,
77
+ };
78
+
79
+ return manager;
80
+ };
81
+
82
+ factory.inject = <S extends object, D extends object>(source: S, dest: D): D & S => {
83
+ Object.keys(source).forEach((k) => {
84
+ // @ts-ignore
85
+ dest[k] = source[k];
86
+ });
87
+ return dest as D & S;
88
+ };
89
+
90
+ factory.create = (options: { service?: ServiceMap; initialState?: FeatureState<T> } = {}) => {
91
+ const { service, initialState } = options;
92
+ const sliceName = factory.entity;
93
+
94
+ const standard: ServiceMap = {
95
+ fetch: factory.thunk.create(sliceName, 'all'),
96
+ fetchOne: factory.thunk.create(sliceName, 'one'),
97
+ search: factory.thunk.create(sliceName, 'search'),
98
+ };
99
+
100
+ const slice = createSlice({
101
+ name: sliceName,
102
+ initialState: initialState ?? StateFactory<T>(sliceName),
103
+ reducers: factory.reducers,
104
+ extraReducers: (builder) => {
105
+ // handle standard thunks
106
+ Object.entries(standard).forEach(([act, thunk]) => {
107
+ builder
108
+ .addCase(thunk.pending, (state) => {
109
+ state.loading = true;
110
+ state.error = null;
111
+ })
112
+ .addCase(thunk.fulfilled, (state, action: PayloadAction<T[]>) => {
113
+ state.loading = false;
114
+ state.success = true;
115
+ state.data = action.payload;
116
+ state[sliceName] = action.payload[0];
117
+ })
118
+ .addCase(thunk.rejected, (state, action) => {
119
+ state.loading = false;
120
+ state.success = false;
121
+ state.error = action.error;
122
+ });
123
+ });
124
+
125
+ // handle extra service thunks
126
+ if (service) {
127
+ Object.entries(service).forEach(([key, thunk]) => {
128
+ if (!standard[key]) {
129
+ builder
130
+ .addCase(thunk.pending, (state) => {
131
+ state.loading = true;
132
+ state.error = null;
133
+ })
134
+ .addCase(thunk.fulfilled, (state, action: PayloadAction<any>) => {
135
+ state.loading = false;
136
+ state.success = true;
137
+ state.data = action.payload;
138
+ console.log('action fulfilled :', state.data);
139
+ })
140
+ .addCase(thunk.rejected, (state, action) => {
141
+ state.loading = false;
142
+ state.success = false;
143
+ state.error = action.payload;
144
+ });
145
+ }
146
+ });
147
+ }
148
+ },
149
+ });
150
+
151
+ const asyncActions = factory.inject(standard, service ?? {});
152
+ return { slice, asyncActions };
153
+ };
154
+
155
+ return factory;
156
+ }
157
+
158
+ export default FeatureFactory;