@nlabs/metropolisjs 1.0.8 → 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/README.md +75 -40
- package/docs/ACTIONS.md +61 -1
- 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/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 +3 -8
- package/lib/index.js +4 -9
- 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/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
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# MetropolisJS: Seamless Frontend-Backend Integration Framework
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src="https://raw.githubusercontent.com/nitrogenlabs/metropolisjs/main/docs/assets/metropolisjs-
|
|
4
|
+
<img src="https://raw.githubusercontent.com/nitrogenlabs/metropolisjs/main/docs/assets/metropolisjs-mark.svg" alt="MetropolisJS winged V logo" width="432">
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
> **The Ultimate Frontend Integration Library for Modern Web Applications**
|
|
@@ -183,7 +183,7 @@ Configure the public analytics identifier returned by Reaktor under `app.rum.ana
|
|
|
183
183
|
</Metropolis>
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
Configure the endpoint at `app.api.endpoints.rum`. RUM delivery is unauthenticated and each batch contains `analyticsId` and up to 50 sanitized events.
|
|
187
187
|
|
|
188
188
|
#### Beacon delivery
|
|
189
189
|
|
|
@@ -450,7 +450,7 @@ const LoginForm = () => {
|
|
|
450
450
|
|
|
451
451
|
const handleLogin = async () => {
|
|
452
452
|
try {
|
|
453
|
-
const session = await userActions.signIn(
|
|
453
|
+
const session = await userActions.signIn({password, username});
|
|
454
454
|
console.log('User logged in successfully!', session);
|
|
455
455
|
} catch (error) {
|
|
456
456
|
console.error('Login failed:', error);
|
|
@@ -476,6 +476,41 @@ const LoginForm = () => {
|
|
|
476
476
|
};
|
|
477
477
|
```
|
|
478
478
|
|
|
479
|
+
### Billing Setup Sessions
|
|
480
|
+
|
|
481
|
+
Billing cards are collected through a hosted setup session, so raw card details never pass through application code. Start the flow with an authenticated user action and redirect the browser to the returned checkout URL:
|
|
482
|
+
|
|
483
|
+
```tsx
|
|
484
|
+
import {useUserActions} from '@nlabs/metropolisjs';
|
|
485
|
+
|
|
486
|
+
const AddBillingCardButton = () => {
|
|
487
|
+
const userActions = useUserActions();
|
|
488
|
+
|
|
489
|
+
const addBillingCard = async () => {
|
|
490
|
+
const returnUrl = `${window.location.origin}/settings/billing/complete`;
|
|
491
|
+
const checkoutUrl = await userActions.createBillingSetupSession(returnUrl);
|
|
492
|
+
window.location.assign(checkoutUrl);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
return <button onClick={addBillingCard}>Add billing card</button>;
|
|
496
|
+
};
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
On the return page, read the provider's setup-session identifier and complete the flow. The action returns the updated user, refreshes the matching session data, dispatches the standard user update event, and clears related user request caches:
|
|
500
|
+
|
|
501
|
+
```tsx
|
|
502
|
+
const sessionId = new URLSearchParams(window.location.search).get('session_id');
|
|
503
|
+
|
|
504
|
+
if(sessionId) {
|
|
505
|
+
const user = await userActions.completeBillingSetupSession(sessionId, [
|
|
506
|
+
'stripeCardBrand',
|
|
507
|
+
'stripeCardLast4'
|
|
508
|
+
]);
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Use `deleteBillingCard()` to remove the saved billing method. Both completion and deletion return sanitized billing metadata; MetropolisJS does not accept raw card numbers or tokens.
|
|
513
|
+
|
|
479
514
|
### Real-Time Messaging
|
|
480
515
|
|
|
481
516
|
```tsx
|
|
@@ -597,21 +632,34 @@ MetropolisJS uses a **factory function pattern** for actions. This provides func
|
|
|
597
632
|
### Basic Usage
|
|
598
633
|
|
|
599
634
|
```typescript
|
|
600
|
-
import {createUserActions} from '
|
|
635
|
+
import {createAction, createActions, createUserActions} from '@nlabs/metropolisjs';
|
|
601
636
|
|
|
602
637
|
const userActions = createUserActions(flux);
|
|
603
|
-
const user = await userActions.
|
|
638
|
+
const user = await userActions.addUser(userData);
|
|
639
|
+
|
|
640
|
+
const postActions = createAction('post', flux);
|
|
641
|
+
const post = await postActions.add({content: 'Hello!'});
|
|
642
|
+
|
|
643
|
+
const actions = createActions(['user', 'post', 'message'], flux);
|
|
644
|
+
await actions.message.sendMessage({
|
|
645
|
+
content: 'Welcome!',
|
|
646
|
+
recipientId: user.userId
|
|
647
|
+
});
|
|
604
648
|
```
|
|
605
649
|
|
|
650
|
+
Factory results preserve their selected types. `createAction('post', flux)` returns `PostActions`, while `createActions(['user', 'post'], flux)` returns only typed `user` and `post` keys. `createAllActions(flux)` returns the complete `ActionMap`.
|
|
651
|
+
|
|
606
652
|
### Advanced Usage with Custom Adapters
|
|
607
653
|
|
|
608
654
|
#### Custom Validation Adapter
|
|
609
655
|
|
|
610
656
|
```typescript
|
|
657
|
+
import type {User} from '@nlabs/metropolisjs';
|
|
658
|
+
|
|
611
659
|
// Custom adapter that extends default behavior
|
|
612
|
-
const customUserAdapter = (input: unknown
|
|
660
|
+
const customUserAdapter = (input: unknown): User => {
|
|
613
661
|
// input is already validated by default adapter
|
|
614
|
-
const user = input as
|
|
662
|
+
const user = input as User;
|
|
615
663
|
|
|
616
664
|
// Add business-specific validation
|
|
617
665
|
if (user.email && !user.email.includes('@company.com')) {
|
|
@@ -622,7 +670,7 @@ const customUserAdapter = (input: unknown, options?: UserAdapterOptions) => {
|
|
|
622
670
|
return {
|
|
623
671
|
...user,
|
|
624
672
|
fullName: `${user.firstName || ''} ${user.lastName || ''}`.trim(),
|
|
625
|
-
isAdmin: user.userAccess >= 3
|
|
673
|
+
isAdmin: (user.userAccess || 0) >= 3
|
|
626
674
|
};
|
|
627
675
|
};
|
|
628
676
|
|
|
@@ -688,31 +736,7 @@ interface AdapterOptions {
|
|
|
688
736
|
}
|
|
689
737
|
```
|
|
690
738
|
|
|
691
|
-
###
|
|
692
|
-
|
|
693
|
-
#### Step 1: Update Imports
|
|
694
|
-
|
|
695
|
-
```typescript
|
|
696
|
-
// Old
|
|
697
|
-
import {userActions} from '../actions/userActions';
|
|
698
|
-
|
|
699
|
-
// New
|
|
700
|
-
import {createUserActions} from '../actions/userActions';
|
|
701
|
-
```
|
|
702
|
-
|
|
703
|
-
#### Step 2: Update Instantiation
|
|
704
|
-
|
|
705
|
-
```typescript
|
|
706
|
-
// Old
|
|
707
|
-
const userActions = new userActions(flux, customAdapter);
|
|
708
|
-
|
|
709
|
-
// New
|
|
710
|
-
const userActions = createUserActions(flux, {
|
|
711
|
-
userAdapter: customAdapter
|
|
712
|
-
});
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
#### Step 3: Using Actions in Components
|
|
739
|
+
### Using Actions in Components
|
|
716
740
|
|
|
717
741
|
The recommended approach is to use specialized hooks:
|
|
718
742
|
|
|
@@ -765,11 +789,11 @@ const apiUrl = config.app?.api?.url || '';
|
|
|
765
789
|
#### Unit Testing Actions
|
|
766
790
|
|
|
767
791
|
```typescript
|
|
768
|
-
import {createUserActions} from '@nlabs/metropolisjs';
|
|
792
|
+
import {createUserActions, type UserActions} from '@nlabs/metropolisjs';
|
|
769
793
|
|
|
770
794
|
describe('userActions', () => {
|
|
771
795
|
let flux: FluxFramework;
|
|
772
|
-
let userActions:
|
|
796
|
+
let userActions: UserActions;
|
|
773
797
|
|
|
774
798
|
beforeEach(() => {
|
|
775
799
|
flux = createMockFlux();
|
|
@@ -782,7 +806,7 @@ describe('userActions', () => {
|
|
|
782
806
|
|
|
783
807
|
it('should add user with validation', async () => {
|
|
784
808
|
const userData = {username: 'test', email: 'test@example.com'};
|
|
785
|
-
const result = await userActions.
|
|
809
|
+
const result = await userActions.addUser(userData);
|
|
786
810
|
expect(result).toBeDefined();
|
|
787
811
|
});
|
|
788
812
|
});
|
|
@@ -791,7 +815,7 @@ describe('userActions', () => {
|
|
|
791
815
|
#### Testing with Custom Adapters
|
|
792
816
|
|
|
793
817
|
```typescript
|
|
794
|
-
const mockAdapter =
|
|
818
|
+
const mockAdapter = vi.fn((input) => ({
|
|
795
819
|
...input,
|
|
796
820
|
validated: true
|
|
797
821
|
}));
|
|
@@ -1267,8 +1291,8 @@ For more detailed examples, see [`examples/permission-system-usage.tsx`](./examp
|
|
|
1267
1291
|
### Prerequisites
|
|
1268
1292
|
|
|
1269
1293
|
- Node.js 16+
|
|
1270
|
-
- React
|
|
1271
|
-
- TypeScript
|
|
1294
|
+
- React 19+
|
|
1295
|
+
- TypeScript 7+
|
|
1272
1296
|
|
|
1273
1297
|
### Full Installation
|
|
1274
1298
|
|
|
@@ -1292,7 +1316,18 @@ Configure your environment-specific settings in the `config` prop of the `Metrop
|
|
|
1292
1316
|
|
|
1293
1317
|
## Contributing
|
|
1294
1318
|
|
|
1295
|
-
|
|
1319
|
+
Before opening a pull request, run the same quality gates used for source, tests, examples, and the published declarations:
|
|
1320
|
+
|
|
1321
|
+
```bash
|
|
1322
|
+
npm run lint
|
|
1323
|
+
npm run typecheck
|
|
1324
|
+
npm test
|
|
1325
|
+
npm run build
|
|
1326
|
+
```
|
|
1327
|
+
|
|
1328
|
+
`npm run typecheck` checks the production source, unit and integration tests, lint inputs, and every file under `examples/`.
|
|
1329
|
+
|
|
1330
|
+
To contribute:
|
|
1296
1331
|
|
|
1297
1332
|
1. **Fork** the repository
|
|
1298
1333
|
2. **Create** a feature branch (`git checkout -b feature/amazing-feature`)
|
package/docs/ACTIONS.md
CHANGED
|
@@ -26,6 +26,26 @@ const postActions = createPostActions(flux);
|
|
|
26
26
|
const restActions = createRestActions(flux);
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
### Typed Consolidated Factories
|
|
30
|
+
|
|
31
|
+
The consolidated factories preserve the selected action types without casts:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import {createAction, createActions, createAllActions} from '@nlabs/metropolisjs';
|
|
35
|
+
|
|
36
|
+
const userActions = createAction('user', flux);
|
|
37
|
+
await userActions.addUser({username: 'ada'});
|
|
38
|
+
|
|
39
|
+
const actions = createActions(['user', 'post', 'message'], flux);
|
|
40
|
+
await actions.post.add({content: 'Hello!'});
|
|
41
|
+
await actions.message.sendMessage({content: 'Welcome!'});
|
|
42
|
+
|
|
43
|
+
const allActions = createAllActions(flux);
|
|
44
|
+
await allActions.permission.list();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`createAction()` maps its key to the corresponding action interface, `createActions()` returns exactly the requested keys, and `createAllActions()` returns the complete exported `ActionMap`.
|
|
48
|
+
|
|
29
49
|
## Action Families
|
|
30
50
|
|
|
31
51
|
Each row links to:
|
|
@@ -56,7 +76,7 @@ These action families are available through specialized hooks when present, `use
|
|
|
56
76
|
| Subscription | `useSubscriptionActions` | `subscription` | `createSubscriptionActions` | `addPlan`, `getPlanByItem`, `addSubscription`, `getSubscriptionByItem`, `getSubscriptionListByUser`, `deleteSubscription` | [subscriptionActions.ts](../src/actions/subscriptionActions/subscriptionActions.ts) |
|
|
57
77
|
| Tag | `useTagActions` | `tag` | `createTagActions` | `addTag`, `addTagToItem`, `getTags`, `updateTag`, `deleteTag`, `deleteTagFromItem` | [tagActions.ts](../src/actions/tagActions/tagActions.ts) |
|
|
58
78
|
| Translation | `useTranslationActions` | `translation` | `createTranslationActions` | `addTranslations`, `getTranslation`, `getTranslations`, `hasTranslation`, `queueTranslationKey`, `processPendingTranslations` | [translationActions.ts](../src/actions/translationActions/translationActions.ts) |
|
|
59
|
-
| User | `useUserActions` | `user` | `createUserActions` | `signIn`, `signUp`, `session`, `
|
|
79
|
+
| User | `useUserActions` | `user` | `createUserActions` | `signIn`, `signUp`, `session`, `createBillingSetupSession`, `completeBillingSetupSession`, `deleteBillingCard`, `itemById`, `updateUser` | [userActions.ts](../src/actions/userActions/userActions.ts) |
|
|
60
80
|
| Video | `useVideoActions` | `video` | `createVideoActions` | `add`, `itemById`, `list`, `update`, `delete` | [videoActions.ts](../src/actions/videoActions/videoActions.ts) |
|
|
61
81
|
| Websocket | `useWebsocketActions` | `websocket` | `createWebsocketActions` | `wsInit`, `wsSend`, `onOpen`, `onReceive`, `onClose`, `onError` | [websocketActions.ts](../src/actions/websocketActions/websocketActions.ts) |
|
|
62
82
|
|
|
@@ -121,6 +141,34 @@ Configure the action through the `Metropolis` provider:
|
|
|
121
141
|
|
|
122
142
|
The `Metropolis` provider requests beacon delivery automatically on `pagehide` and when `document.visibilityState` changes to `hidden`. A batch accepted by the Beacon API is not submitted a second time. All RUM delivery remains subject to `enabled`, `respectPrivacySignals`, batching, throttling, deduplication, and event sanitization.
|
|
123
143
|
|
|
144
|
+
## User Billing Setup Sessions
|
|
145
|
+
|
|
146
|
+
Use the authenticated user actions to collect a billing method through the hosted setup flow:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import {createUserActions} from '@nlabs/metropolisjs';
|
|
150
|
+
|
|
151
|
+
const userActions = createUserActions(flux);
|
|
152
|
+
const checkoutUrl = await userActions.createBillingSetupSession(
|
|
153
|
+
'https://app.example.com/settings/billing/complete'
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
window.location.assign(checkoutUrl);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
After the billing provider redirects back, complete the session with its identifier:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const user = await userActions.completeBillingSetupSession(
|
|
163
|
+
setupSessionId,
|
|
164
|
+
['stripeCardBrand', 'stripeCardLast4']
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`createBillingSetupSession(returnUrl)` validates the return URL and resolves to the hosted checkout URL. `completeBillingSetupSession(sessionId, userProps?, requestOptions?)` validates the identifier, returns the updated user, synchronizes the active session when it belongs to that user, dispatches `USER_UPDATE_ITEM_SUCCESS`, and clears related request caches. `deleteBillingCard(userProps?, requestOptions?)` removes the stored method through the same authenticated action family.
|
|
169
|
+
|
|
170
|
+
These APIs exchange setup-session identifiers and sanitized billing metadata only. They do not accept raw card details.
|
|
171
|
+
|
|
124
172
|
## REST Actions
|
|
125
173
|
|
|
126
174
|
Use REST actions for external APIs that are not represented in Reaktor. REST actions delegate to `@nlabs/rip-hunter`, share Metropolis network/session handling, and can target either a configured endpoint key or an absolute URL.
|
|
@@ -173,6 +221,18 @@ All action creators are re-exported from:
|
|
|
173
221
|
|
|
174
222
|
- [src/actions/index.ts](../src/actions/index.ts)
|
|
175
223
|
|
|
224
|
+
The package root also exports every creator, action interface, `ActionMap`, and the consolidated factory functions.
|
|
225
|
+
|
|
176
226
|
All specialized hooks are exposed from:
|
|
177
227
|
|
|
178
228
|
- [src/utils/useMetropolis.ts](../src/utils/useMetropolis.ts)
|
|
229
|
+
|
|
230
|
+
## Development Type Checks
|
|
231
|
+
|
|
232
|
+
Run the complete TypeScript gate with:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
npm run typecheck
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
This validates the production source, tests, lint inputs, and examples. Run `npm run lint`, `npm test`, and `npm run build` before publishing.
|
|
Binary file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 600" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">MetropolisJS</title>
|
|
3
|
+
<desc id="desc">Purple winged V mark representing the connection between frontend and backend systems.</desc>
|
|
4
|
+
<path
|
|
5
|
+
d="M600 280C585 280 575 270 565 250C525 170 480 100 415 70C318 25 205 60 125 155C82 206 55 278 40 340C95 250 170 205 245 205C365 205 470 360 555 505C570 532 582 548 600 548C618 548 630 532 645 505C730 360 835 205 955 205C1030 205 1105 250 1160 340C1145 278 1118 206 1075 155C995 60 882 25 785 70C720 100 675 170 635 250C625 270 615 280 600 280Z"
|
|
6
|
+
fill="#712CF9"/>
|
|
7
|
+
</svg>
|
package/examples/crud-usage.tsx
CHANGED
|
@@ -700,7 +700,7 @@ export const EventManagerExample = () => {
|
|
|
700
700
|
<p>
|
|
701
701
|
{new Date(event.startDate!).toLocaleString()} - {new Date(event.endDate!).toLocaleString()}
|
|
702
702
|
</p>
|
|
703
|
-
<p>Location: {event.location}</p>
|
|
703
|
+
<p>Location: {typeof event.location === 'string' ? event.location : event.location?.address}</p>
|
|
704
704
|
<button onClick={() => handleDeleteEvent(event.eventId!)}>Delete</button>
|
|
705
705
|
</div>
|
|
706
706
|
))}
|
|
@@ -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
|
};
|