@nlabs/metropolisjs 1.0.7 → 1.1.0
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/CHANGELOG.md +7 -0
- package/README.md +128 -42
- package/docs/ACTIONS.md +116 -3
- package/docs/assets/metropolisjs-logo.png +0 -0
- package/docs/assets/metropolisjs-mark.svg +7 -0
- package/examples/crud-usage.tsx +1 -1
- package/examples/factory-pattern-usage.ts +85 -78
- package/examples/signup-error-handling.ts +25 -24
- package/factoryPatternGuide.md +90 -174
- package/lib/actions/awsRumActions/awsRumActions.d.ts +8 -1
- package/lib/actions/awsRumActions/awsRumActions.js +9 -5
- package/lib/actions/translationActions/translationActions.d.ts +2 -11
- package/lib/actions/translationActions/translationActions.js +2 -2
- package/lib/actions/userActions/userActions.d.ts +7 -5
- package/lib/actions/userActions/userActions.js +111 -64
- package/lib/index.d.ts +4 -9
- package/lib/index.js +19 -10
- package/lib/stores/tagStore.d.ts +1 -0
- package/lib/stores/tagStore.js +1 -1
- package/lib/stores/userStore.d.ts +1 -0
- package/lib/stores/userStore.js +2 -1
- package/lib/utils/actionFactory.d.ts +52 -6
- package/lib/utils/actionFactory.js +8 -6
- package/lib/utils/api.d.ts +7 -0
- package/lib/utils/api.js +18 -1
- package/lib/utils/baseActionFactory.d.ts +1 -1
- package/lib/utils/index.d.ts +1 -1
- package/lib/utils/index.js +2 -2
- package/lib/utils/session.d.ts +1 -1
- package/package.json +16 -14
- package/tsconfig.examples.json +23 -0
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Example: Using the Factory Pattern in MetropolisJS
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This file demonstrates how to use the new consolidated action factory
|
|
5
5
|
* instead of individual createXxxActions functions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
|
|
8
|
+
import {createAction, createActions, createAllActions} from '../src/utils/actionFactory.js';
|
|
9
|
+
|
|
10
|
+
import type {FluxFramework} from '@nlabs/arkhamjs';
|
|
11
|
+
import type {PostType} from '../src/adapters/postAdapter/postAdapter.js';
|
|
12
|
+
import type {User} from '../src/adapters/userAdapter/userAdapter.js';
|
|
13
|
+
|
|
14
|
+
const getErrorMessage = (error: unknown): string => (
|
|
15
|
+
error instanceof Error ? error.message : 'An unknown error occurred'
|
|
16
|
+
);
|
|
10
17
|
|
|
11
18
|
// Example 1: Basic Usage
|
|
12
19
|
export const basicUsage = (flux: FluxFramework) => {
|
|
13
20
|
// Create actions using consolidated factory functions
|
|
14
|
-
const userActions = createAction('user', flux)
|
|
15
|
-
const postActions = createAction('post', flux)
|
|
16
|
-
const eventActions = createAction('event', flux) as any;
|
|
21
|
+
const userActions = createAction('user', flux);
|
|
22
|
+
const postActions = createAction('post', flux);
|
|
17
23
|
|
|
18
24
|
// Use actions normally
|
|
19
25
|
const addUser = async () => {
|
|
20
|
-
const user = await userActions.
|
|
21
|
-
username: 'john_doe',
|
|
26
|
+
const user = await userActions.addUser({
|
|
22
27
|
email: 'john@example.com',
|
|
23
28
|
firstName: 'John',
|
|
24
|
-
lastName: 'Doe'
|
|
29
|
+
lastName: 'Doe',
|
|
30
|
+
username: 'john_doe'
|
|
25
31
|
});
|
|
26
32
|
return user;
|
|
27
33
|
};
|
|
@@ -40,20 +46,21 @@ export const basicUsage = (flux: FluxFramework) => {
|
|
|
40
46
|
// Example 2: Custom Adapter with Business Logic
|
|
41
47
|
export const customAdapterUsage = (flux: FluxFramework) => {
|
|
42
48
|
// Custom user adapter that adds business logic
|
|
43
|
-
const businessUserAdapter = (input: unknown
|
|
44
|
-
const user = input as
|
|
45
|
-
|
|
49
|
+
const businessUserAdapter = (input: unknown): User => {
|
|
50
|
+
const user = input as User;
|
|
51
|
+
const email = user.email || '';
|
|
52
|
+
|
|
46
53
|
// Business validation
|
|
47
|
-
if
|
|
54
|
+
if(email && !email.includes('@company.com')) {
|
|
48
55
|
throw new Error('Only company emails are allowed');
|
|
49
56
|
}
|
|
50
|
-
|
|
57
|
+
|
|
51
58
|
// Add computed fields
|
|
52
59
|
return {
|
|
53
60
|
...user,
|
|
61
|
+
department: email.split('@')[0]?.split('.')[1] || 'general',
|
|
54
62
|
fullName: `${user.firstName || ''} ${user.lastName || ''}`.trim(),
|
|
55
|
-
isAdmin: user.userAccess >= 3
|
|
56
|
-
department: user.email?.split('@')[0]?.split('.')[1] || 'general'
|
|
63
|
+
isAdmin: (user.userAccess || 0) >= 3
|
|
57
64
|
};
|
|
58
65
|
};
|
|
59
66
|
|
|
@@ -61,8 +68,8 @@ export const customAdapterUsage = (flux: FluxFramework) => {
|
|
|
61
68
|
const userActions = createAction('user', flux, {
|
|
62
69
|
userAdapter: businessUserAdapter,
|
|
63
70
|
userAdapterOptions: {
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
environment: 'production',
|
|
72
|
+
strict: true
|
|
66
73
|
}
|
|
67
74
|
});
|
|
68
75
|
|
|
@@ -71,73 +78,72 @@ export const customAdapterUsage = (flux: FluxFramework) => {
|
|
|
71
78
|
|
|
72
79
|
// Example 3: Runtime Adapter Updates
|
|
73
80
|
export const runtimeUpdates = (flux: FluxFramework) => {
|
|
74
|
-
const userActions = createAction('user', flux)
|
|
81
|
+
const userActions = createAction('user', flux);
|
|
75
82
|
|
|
76
83
|
// Update adapter at runtime
|
|
77
84
|
const updateToStrictMode = () => {
|
|
78
85
|
userActions.updateUserAdapterOptions({
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
environment: 'production',
|
|
87
|
+
strict: true
|
|
81
88
|
});
|
|
82
89
|
};
|
|
83
90
|
|
|
84
91
|
// Add custom validation at runtime
|
|
85
92
|
const addCustomValidation = () => {
|
|
86
|
-
userActions.updateUserAdapter((input
|
|
87
|
-
const user = input as
|
|
88
|
-
|
|
93
|
+
userActions.updateUserAdapter((input) => {
|
|
94
|
+
const user = input as User;
|
|
95
|
+
|
|
89
96
|
// Additional runtime validation
|
|
90
|
-
if
|
|
97
|
+
if(user.age && user.age < 18) {
|
|
91
98
|
throw new Error('User must be 18 or older');
|
|
92
99
|
}
|
|
93
|
-
|
|
100
|
+
|
|
94
101
|
return user;
|
|
95
102
|
});
|
|
96
103
|
};
|
|
97
104
|
|
|
98
|
-
return {
|
|
105
|
+
return {addCustomValidation, updateToStrictMode, userActions};
|
|
99
106
|
};
|
|
100
107
|
|
|
101
108
|
// Example 4: Testing with Mock Adapters
|
|
102
109
|
export const testingExample = (flux: FluxFramework) => {
|
|
103
110
|
// Mock adapter for testing
|
|
104
|
-
const
|
|
105
|
-
|
|
111
|
+
const calls: unknown[] = [];
|
|
112
|
+
const mockUserAdapter = (input: unknown) => {
|
|
113
|
+
calls.push(input);
|
|
106
114
|
return {
|
|
107
115
|
...(input as Record<string, unknown>),
|
|
108
116
|
id: 'mock-user-id',
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
validated: true
|
|
111
119
|
};
|
|
112
|
-
}
|
|
113
|
-
calls: [] as unknown[]
|
|
114
|
-
});
|
|
120
|
+
};
|
|
115
121
|
|
|
116
122
|
const userActions = createAction('user', flux, {
|
|
117
123
|
userAdapter: mockUserAdapter
|
|
118
|
-
})
|
|
124
|
+
});
|
|
119
125
|
|
|
120
126
|
// Test that adapter was called
|
|
121
127
|
const testUserCreation = async () => {
|
|
122
|
-
const user = await userActions.
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
const user = await userActions.addUser({
|
|
129
|
+
email: 'test@example.com',
|
|
130
|
+
username: 'test_user'
|
|
125
131
|
});
|
|
126
132
|
|
|
127
|
-
if(
|
|
133
|
+
if(calls.length === 0 || user.id !== 'mock-user-id' || user.validated !== true) {
|
|
128
134
|
throw new Error('Mock adapter validation failed');
|
|
129
135
|
}
|
|
130
136
|
};
|
|
131
137
|
|
|
132
|
-
return {
|
|
138
|
+
return {testUserCreation, userActions};
|
|
133
139
|
};
|
|
134
140
|
|
|
135
141
|
// Example 5: Multiple Adapters with Different Configurations
|
|
136
142
|
export const multipleAdapters = (flux: FluxFramework) => {
|
|
137
143
|
// User adapter with strict validation
|
|
138
144
|
const strictUserAdapter = (input: unknown) => {
|
|
139
|
-
const user = input as
|
|
140
|
-
if
|
|
145
|
+
const user = input as User;
|
|
146
|
+
if(!user.username || !user.email) {
|
|
141
147
|
throw new Error('Username and email are required');
|
|
142
148
|
}
|
|
143
149
|
return user;
|
|
@@ -145,8 +151,8 @@ export const multipleAdapters = (flux: FluxFramework) => {
|
|
|
145
151
|
|
|
146
152
|
// Post adapter with content validation
|
|
147
153
|
const contentPostAdapter = (input: unknown) => {
|
|
148
|
-
const post = input as
|
|
149
|
-
if
|
|
154
|
+
const post = input as PostType;
|
|
155
|
+
if(post.content && post.content.length > 1000) {
|
|
150
156
|
throw new Error('Post content too long');
|
|
151
157
|
}
|
|
152
158
|
return post;
|
|
@@ -156,44 +162,44 @@ export const multipleAdapters = (flux: FluxFramework) => {
|
|
|
156
162
|
const userActions = createAction('user', flux, {
|
|
157
163
|
userAdapter: strictUserAdapter,
|
|
158
164
|
userAdapterOptions: {strict: true}
|
|
159
|
-
})
|
|
165
|
+
});
|
|
160
166
|
|
|
161
167
|
const postActions = createAction('post', flux, {
|
|
162
168
|
postAdapter: contentPostAdapter,
|
|
163
169
|
postAdapterOptions: {environment: 'development'}
|
|
164
|
-
})
|
|
170
|
+
});
|
|
165
171
|
|
|
166
|
-
return {
|
|
172
|
+
return {postActions, userActions};
|
|
167
173
|
};
|
|
168
174
|
|
|
169
175
|
// Example 6: Error Handling
|
|
170
176
|
export const errorHandling = (flux: FluxFramework) => {
|
|
171
177
|
const userActions = createAction('user', flux, {
|
|
172
178
|
userAdapterOptions: {
|
|
173
|
-
strict: true,
|
|
174
179
|
customValidation: (input) => {
|
|
175
|
-
const user = input as
|
|
176
|
-
|
|
180
|
+
const user = input as User;
|
|
181
|
+
|
|
177
182
|
// Custom error handling
|
|
178
|
-
if
|
|
183
|
+
if(user.username && user.username.length < 3) {
|
|
179
184
|
throw new Error('Username must be at least 3 characters');
|
|
180
185
|
}
|
|
181
|
-
|
|
182
|
-
if
|
|
186
|
+
|
|
187
|
+
if(user.email && !user.email.includes('@')) {
|
|
183
188
|
throw new Error('Invalid email format');
|
|
184
189
|
}
|
|
185
|
-
|
|
190
|
+
|
|
186
191
|
return user;
|
|
187
|
-
}
|
|
192
|
+
},
|
|
193
|
+
strict: true
|
|
188
194
|
}
|
|
189
|
-
})
|
|
195
|
+
});
|
|
190
196
|
|
|
191
|
-
const createUserWithErrorHandling = async (userData:
|
|
197
|
+
const createUserWithErrorHandling = async (userData: Partial<User>) => {
|
|
192
198
|
try {
|
|
193
|
-
const user = await userActions.
|
|
199
|
+
const user = await userActions.addUser(userData);
|
|
194
200
|
return {success: true, user};
|
|
195
|
-
} catch
|
|
196
|
-
return {
|
|
201
|
+
} catch(error) {
|
|
202
|
+
return {error: getErrorMessage(error), success: false};
|
|
197
203
|
}
|
|
198
204
|
};
|
|
199
205
|
|
|
@@ -204,45 +210,46 @@ export const errorHandling = (flux: FluxFramework) => {
|
|
|
204
210
|
export const multipleActionsExample = (flux: FluxFramework) => {
|
|
205
211
|
// Create multiple actions at once
|
|
206
212
|
const actions = createActions(['user', 'post', 'message'], flux, {
|
|
207
|
-
user: {
|
|
208
|
-
userAdapterOptions: { strict: true }
|
|
209
|
-
},
|
|
210
213
|
post: {
|
|
211
|
-
postAdapter: (input:
|
|
212
|
-
|
|
214
|
+
postAdapter: (input: unknown) => {
|
|
215
|
+
const post = input as PostType;
|
|
216
|
+
if(post.content && post.content.length > 1000) {
|
|
213
217
|
throw new Error('Post content too long');
|
|
214
218
|
}
|
|
215
|
-
return
|
|
219
|
+
return post;
|
|
216
220
|
}
|
|
221
|
+
},
|
|
222
|
+
user: {
|
|
223
|
+
userAdapterOptions: {strict: true}
|
|
217
224
|
}
|
|
218
225
|
});
|
|
219
226
|
|
|
220
|
-
const createUserAndPost = async (userData:
|
|
221
|
-
const user = await actions.user.
|
|
227
|
+
const createUserAndPost = async (userData: Partial<User>, postData: Partial<PostType>) => {
|
|
228
|
+
const user = await actions.user.addUser(userData);
|
|
222
229
|
const post = await actions.post.add({
|
|
223
230
|
...postData,
|
|
224
231
|
userId: user.userId
|
|
225
232
|
});
|
|
226
|
-
return { user
|
|
233
|
+
return {post, user};
|
|
227
234
|
};
|
|
228
235
|
|
|
229
|
-
return {
|
|
236
|
+
return {actions, createUserAndPost};
|
|
230
237
|
};
|
|
231
238
|
|
|
232
239
|
// Example 8: All Actions Creation
|
|
233
240
|
export const allActionsExample = (flux: FluxFramework) => {
|
|
234
241
|
// Create all available actions
|
|
235
242
|
const allActions = createAllActions(flux, {
|
|
236
|
-
|
|
237
|
-
post: {
|
|
238
|
-
|
|
243
|
+
image: {imageAdapterOptions: {allowPartial: true}},
|
|
244
|
+
post: {postAdapterOptions: {environment: 'production'}},
|
|
245
|
+
user: {userAdapterOptions: {strict: true}}
|
|
239
246
|
});
|
|
240
247
|
|
|
241
248
|
const comprehensiveWorkflow = async () => {
|
|
242
249
|
// Create user
|
|
243
|
-
const user = await allActions.user.
|
|
244
|
-
|
|
245
|
-
|
|
250
|
+
const user = await allActions.user.addUser({
|
|
251
|
+
email: 'jane@example.com',
|
|
252
|
+
username: 'jane_doe'
|
|
246
253
|
});
|
|
247
254
|
|
|
248
255
|
// Create post
|
|
@@ -263,8 +270,8 @@ export const allActionsExample = (flux: FluxFramework) => {
|
|
|
263
270
|
recipientId: user.userId
|
|
264
271
|
});
|
|
265
272
|
|
|
266
|
-
return {
|
|
273
|
+
return {image, post, user};
|
|
267
274
|
};
|
|
268
275
|
|
|
269
|
-
return {
|
|
276
|
+
return {allActions, comprehensiveWorkflow};
|
|
270
277
|
};
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
* Example of handling signup errors in MetropolisJS
|
|
3
3
|
*/
|
|
4
4
|
import {FluxFramework} from '@nlabs/arkhamjs';
|
|
5
|
-
|
|
6
|
-
import {USER_CONSTANTS} from '../src/stores/userStore';
|
|
7
|
-
import {createAction} from '../src/utils/actionFactory';
|
|
5
|
+
|
|
6
|
+
import {USER_CONSTANTS} from '../src/stores/userStore.js';
|
|
7
|
+
import {createAction} from '../src/utils/actionFactory.js';
|
|
8
|
+
|
|
9
|
+
import type {UserActions} from '../src/actions/userActions/userActions.js';
|
|
8
10
|
|
|
9
11
|
// Create a flux instance and user actions
|
|
10
12
|
const flux = new FluxFramework();
|
|
11
|
-
const userActions = createAction('user', flux) as unknown as
|
|
13
|
+
const userActions = createAction('user', flux) as unknown as UserActions;
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Example 1: Basic error handling with try/catch
|
|
@@ -18,9 +20,9 @@ const handleAddWithTryCatch = async (userData: any) => {
|
|
|
18
20
|
const user = await userActions.addUser(userData);
|
|
19
21
|
console.log('Add successful:', user);
|
|
20
22
|
return {success: true, user};
|
|
21
|
-
} catch
|
|
23
|
+
} catch(error) {
|
|
22
24
|
console.error('Add failed:', error.message);
|
|
23
|
-
return {
|
|
25
|
+
return {error: error.message, success: false};
|
|
24
26
|
}
|
|
25
27
|
};
|
|
26
28
|
|
|
@@ -31,25 +33,25 @@ const handleSignUpWithFlux = async (userData: any) => {
|
|
|
31
33
|
// Set up a listener for signup success
|
|
32
34
|
const onSignUpSuccess = (payload: any) => {
|
|
33
35
|
console.log('Signup successful:', payload.user);
|
|
34
|
-
flux.
|
|
35
|
-
flux.
|
|
36
|
+
flux.off(USER_CONSTANTS.SIGN_UP_SUCCESS, onSignUpSuccess);
|
|
37
|
+
flux.off(USER_CONSTANTS.SIGN_UP_ERROR, onSignUpError);
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
// Set up a listener for signup error
|
|
39
41
|
const onSignUpError = (payload: any) => {
|
|
40
42
|
console.error('Signup failed:', payload.error.message);
|
|
41
|
-
flux.
|
|
42
|
-
flux.
|
|
43
|
+
flux.off(USER_CONSTANTS.SIGN_UP_SUCCESS, onSignUpSuccess);
|
|
44
|
+
flux.off(USER_CONSTANTS.SIGN_UP_ERROR, onSignUpError);
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
// Add listeners
|
|
46
|
-
flux.
|
|
47
|
-
flux.
|
|
48
|
+
flux.on(USER_CONSTANTS.SIGN_UP_SUCCESS, onSignUpSuccess);
|
|
49
|
+
flux.on(USER_CONSTANTS.SIGN_UP_ERROR, onSignUpError);
|
|
48
50
|
|
|
49
51
|
// Attempt signup
|
|
50
52
|
try {
|
|
51
53
|
await userActions.signUp(userData);
|
|
52
|
-
} catch
|
|
54
|
+
} catch(error) {
|
|
53
55
|
// Error is already handled by the flux listener
|
|
54
56
|
// This catch is just to prevent the error from propagating
|
|
55
57
|
}
|
|
@@ -62,39 +64,38 @@ const handleSignUpWithValidation = async (userData: any) => {
|
|
|
62
64
|
// Perform custom validation
|
|
63
65
|
const errors: string[] = [];
|
|
64
66
|
|
|
65
|
-
if
|
|
67
|
+
if(!userData.email || !userData.email.includes('@')) {
|
|
66
68
|
errors.push('Invalid email format');
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
if
|
|
71
|
+
if(!userData.password || userData.password.length < 8) {
|
|
70
72
|
errors.push('Password must be at least 8 characters');
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
if
|
|
75
|
+
if(!userData.username || userData.username.length < 3) {
|
|
74
76
|
errors.push('Username must be at least 3 characters');
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
// If validation fails, return errors
|
|
78
|
-
if
|
|
79
|
-
return {success: false
|
|
80
|
+
if(errors.length > 0) {
|
|
81
|
+
return {errors, success: false};
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
// If validation passes, attempt signup
|
|
83
85
|
try {
|
|
84
86
|
const user = await userActions.signUp(userData);
|
|
85
87
|
return {success: true, user};
|
|
86
|
-
} catch
|
|
88
|
+
} catch(error) {
|
|
87
89
|
// Handle specific error types
|
|
88
|
-
if
|
|
89
|
-
return {
|
|
90
|
+
if(error.message.includes('already exists')) {
|
|
91
|
+
return {errors: ['Username or email already in use'], success: false};
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
return {
|
|
94
|
+
return {errors: [error.message], success: false};
|
|
93
95
|
}
|
|
94
96
|
};
|
|
95
97
|
|
|
96
98
|
// Export examples
|
|
97
99
|
export {
|
|
98
|
-
|
|
100
|
+
handleAddWithTryCatch, handleSignUpWithFlux, handleSignUpWithValidation
|
|
99
101
|
};
|
|
100
|
-
|