@legible-sync/example-eda 1.2.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/.eslintrc.js +16 -0
- package/README.md +103 -0
- package/__tests__/integration/ecommerce-flow.test.ts +247 -0
- package/__tests__/unit/EventBus.test.ts +154 -0
- package/__tests__/unit/PluginManager.test.ts +111 -0
- package/__tests__/unit/concepts/User.test.ts +130 -0
- package/dist/core/EventBus.d.ts +16 -0
- package/dist/core/EventBus.d.ts.map +1 -0
- package/dist/core/EventBus.js +44 -0
- package/dist/core/EventBus.js.map +1 -0
- package/dist/core/PluginManager.d.ts +16 -0
- package/dist/core/PluginManager.d.ts.map +1 -0
- package/dist/core/PluginManager.js +37 -0
- package/dist/core/PluginManager.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +145 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/analytics/concepts/Analytics.d.ts +3 -0
- package/dist/plugins/analytics/concepts/Analytics.d.ts.map +1 -0
- package/dist/plugins/analytics/concepts/Analytics.js +43 -0
- package/dist/plugins/analytics/concepts/Analytics.js.map +1 -0
- package/dist/plugins/analytics/index.d.ts +3 -0
- package/dist/plugins/analytics/index.d.ts.map +1 -0
- package/dist/plugins/analytics/index.js +16 -0
- package/dist/plugins/analytics/index.js.map +1 -0
- package/dist/plugins/analytics/syncs/analytics-events.sync.d.ts +3 -0
- package/dist/plugins/analytics/syncs/analytics-events.sync.d.ts.map +1 -0
- package/dist/plugins/analytics/syncs/analytics-events.sync.js +101 -0
- package/dist/plugins/analytics/syncs/analytics-events.sync.js.map +1 -0
- package/dist/plugins/inventory/concepts/Inventory.d.ts +3 -0
- package/dist/plugins/inventory/concepts/Inventory.d.ts.map +1 -0
- package/dist/plugins/inventory/concepts/Inventory.js +60 -0
- package/dist/plugins/inventory/concepts/Inventory.js.map +1 -0
- package/dist/plugins/inventory/index.d.ts +3 -0
- package/dist/plugins/inventory/index.d.ts.map +1 -0
- package/dist/plugins/inventory/index.js +15 -0
- package/dist/plugins/inventory/index.js.map +1 -0
- package/dist/plugins/notifications/concepts/Notification.d.ts +3 -0
- package/dist/plugins/notifications/concepts/Notification.d.ts.map +1 -0
- package/dist/plugins/notifications/concepts/Notification.js +42 -0
- package/dist/plugins/notifications/concepts/Notification.js.map +1 -0
- package/dist/plugins/notifications/index.d.ts +3 -0
- package/dist/plugins/notifications/index.d.ts.map +1 -0
- package/dist/plugins/notifications/index.js +15 -0
- package/dist/plugins/notifications/index.js.map +1 -0
- package/dist/plugins/orders/concepts/Order.d.ts +3 -0
- package/dist/plugins/orders/concepts/Order.d.ts.map +1 -0
- package/dist/plugins/orders/concepts/Order.js +98 -0
- package/dist/plugins/orders/concepts/Order.js.map +1 -0
- package/dist/plugins/orders/index.d.ts +3 -0
- package/dist/plugins/orders/index.d.ts.map +1 -0
- package/dist/plugins/orders/index.js +16 -0
- package/dist/plugins/orders/index.js.map +1 -0
- package/dist/plugins/orders/syncs/order-workflow.sync.d.ts +3 -0
- package/dist/plugins/orders/syncs/order-workflow.sync.d.ts.map +1 -0
- package/dist/plugins/orders/syncs/order-workflow.sync.js +168 -0
- package/dist/plugins/orders/syncs/order-workflow.sync.js.map +1 -0
- package/dist/plugins/payments/concepts/Payment.d.ts +3 -0
- package/dist/plugins/payments/concepts/Payment.d.ts.map +1 -0
- package/dist/plugins/payments/concepts/Payment.js +156 -0
- package/dist/plugins/payments/concepts/Payment.js.map +1 -0
- package/dist/plugins/payments/index.d.ts +3 -0
- package/dist/plugins/payments/index.d.ts.map +1 -0
- package/dist/plugins/payments/index.js +16 -0
- package/dist/plugins/payments/index.js.map +1 -0
- package/dist/plugins/payments/syncs/payment-workflow.sync.d.ts +3 -0
- package/dist/plugins/payments/syncs/payment-workflow.sync.d.ts.map +1 -0
- package/dist/plugins/payments/syncs/payment-workflow.sync.js +264 -0
- package/dist/plugins/payments/syncs/payment-workflow.sync.js.map +1 -0
- package/dist/plugins/products/concepts/Product.d.ts +3 -0
- package/dist/plugins/products/concepts/Product.d.ts.map +1 -0
- package/dist/plugins/products/concepts/Product.js +85 -0
- package/dist/plugins/products/concepts/Product.js.map +1 -0
- package/dist/plugins/products/index.d.ts +3 -0
- package/dist/plugins/products/index.d.ts.map +1 -0
- package/dist/plugins/products/index.js +16 -0
- package/dist/plugins/products/index.js.map +1 -0
- package/dist/plugins/products/syncs/product-events.sync.d.ts +3 -0
- package/dist/plugins/products/syncs/product-events.sync.d.ts.map +1 -0
- package/dist/plugins/products/syncs/product-events.sync.js +77 -0
- package/dist/plugins/products/syncs/product-events.sync.js.map +1 -0
- package/dist/plugins/users/concepts/User.d.ts +3 -0
- package/dist/plugins/users/concepts/User.d.ts.map +1 -0
- package/dist/plugins/users/concepts/User.js +81 -0
- package/dist/plugins/users/concepts/User.js.map +1 -0
- package/dist/plugins/users/index.d.ts +3 -0
- package/dist/plugins/users/index.d.ts.map +1 -0
- package/dist/plugins/users/index.js +16 -0
- package/dist/plugins/users/index.js.map +1 -0
- package/dist/plugins/users/syncs/user-events.sync.d.ts +3 -0
- package/dist/plugins/users/syncs/user-events.sync.d.ts.map +1 -0
- package/dist/plugins/users/syncs/user-events.sync.js +75 -0
- package/dist/plugins/users/syncs/user-events.sync.js.map +1 -0
- package/package.json +40 -0
- package/src/core/EventBus.ts +55 -0
- package/src/core/PluginManager.ts +51 -0
- package/src/index.ts +169 -0
- package/src/plugins/analytics/concepts/Analytics.ts +53 -0
- package/src/plugins/analytics/index.ts +15 -0
- package/src/plugins/analytics/syncs/analytics-events.sync.ts +103 -0
- package/src/plugins/inventory/concepts/Inventory.ts +73 -0
- package/src/plugins/inventory/index.ts +14 -0
- package/src/plugins/notifications/concepts/Notification.ts +49 -0
- package/src/plugins/notifications/index.ts +14 -0
- package/src/plugins/orders/concepts/Order.ts +118 -0
- package/src/plugins/orders/index.ts +15 -0
- package/src/plugins/orders/syncs/order-workflow.sync.ts +173 -0
- package/src/plugins/payments/concepts/Payment.ts +186 -0
- package/src/plugins/payments/index.ts +15 -0
- package/src/plugins/payments/syncs/payment-workflow.sync.ts +274 -0
- package/src/plugins/products/concepts/Product.ts +102 -0
- package/src/plugins/products/index.ts +15 -0
- package/src/plugins/products/syncs/product-events.sync.ts +78 -0
- package/src/plugins/users/concepts/User.ts +97 -0
- package/src/plugins/users/index.ts +15 -0
- package/src/plugins/users/syncs/user-events.sync.ts +76 -0
- package/tsconfig.json +9 -0
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
parser: '@typescript-eslint/parser',
|
|
3
|
+
extends: [
|
|
4
|
+
'eslint:recommended',
|
|
5
|
+
'plugin:@typescript-eslint/recommended'
|
|
6
|
+
],
|
|
7
|
+
plugins: ['@typescript-eslint'],
|
|
8
|
+
env: {
|
|
9
|
+
node: true,
|
|
10
|
+
es6: true
|
|
11
|
+
},
|
|
12
|
+
rules: {
|
|
13
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
14
|
+
'@typescript-eslint/no-unused-vars': 'off'
|
|
15
|
+
}
|
|
16
|
+
};
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Event-Driven Architecture Example
|
|
2
|
+
|
|
3
|
+
This example demonstrates how to build an Event-Driven Architecture (EDA) system using LegibleSync with a plugin-based modular architecture.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview
|
|
6
|
+
|
|
7
|
+
The system is composed of independent plugins that communicate through events:
|
|
8
|
+
|
|
9
|
+
- **Users Plugin**: User management and authentication
|
|
10
|
+
- **Products Plugin**: Product catalog management
|
|
11
|
+
- **Orders Plugin**: Order processing and lifecycle
|
|
12
|
+
- **Inventory Plugin**: Stock management and alerts
|
|
13
|
+
- **Notifications Plugin**: Multi-channel notifications
|
|
14
|
+
- **Analytics Plugin**: Event tracking and reporting
|
|
15
|
+
|
|
16
|
+
## Event Flow
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
User Registration → Welcome Email → Analytics Track
|
|
20
|
+
↓
|
|
21
|
+
Product Added → Inventory Update → Low Stock Alert
|
|
22
|
+
↓
|
|
23
|
+
Order Created → Inventory Deduct → Payment Process → Order Confirmed
|
|
24
|
+
↓
|
|
25
|
+
Notification Sent → Analytics Update
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Plugin Structure
|
|
29
|
+
|
|
30
|
+
Each plugin follows the same structure:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
plugin-name/
|
|
34
|
+
├── concepts/ # Business logic components
|
|
35
|
+
├── syncs/ # Event-driven rules
|
|
36
|
+
└── index.ts # Plugin registration
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Key EDA Patterns Demonstrated
|
|
40
|
+
|
|
41
|
+
### 1. Event Sourcing
|
|
42
|
+
- All business events are captured as ActionRecords
|
|
43
|
+
- Complete audit trail of system activity
|
|
44
|
+
|
|
45
|
+
### 2. CQRS (Command Query Responsibility Segregation)
|
|
46
|
+
- Commands: Write operations (create, update, delete)
|
|
47
|
+
- Queries: Read operations with filtering
|
|
48
|
+
|
|
49
|
+
### 3. Event-Driven Orchestration
|
|
50
|
+
- Declarative sync rules define event flows
|
|
51
|
+
- Automatic execution of business processes
|
|
52
|
+
|
|
53
|
+
### 4. Plugin Isolation
|
|
54
|
+
- Each plugin is independently deployable
|
|
55
|
+
- Loose coupling through event contracts
|
|
56
|
+
|
|
57
|
+
## Running the Example
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cd packages/example-eda
|
|
61
|
+
npm run dev
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This will start the EDA system and demonstrate various event flows.
|
|
65
|
+
|
|
66
|
+
## Testing
|
|
67
|
+
|
|
68
|
+
The example includes comprehensive test suites:
|
|
69
|
+
|
|
70
|
+
### Unit Tests
|
|
71
|
+
```bash
|
|
72
|
+
# Run all unit tests
|
|
73
|
+
npm test
|
|
74
|
+
|
|
75
|
+
# Run specific test file
|
|
76
|
+
npm test -- EventBus.test.ts
|
|
77
|
+
|
|
78
|
+
# Run with coverage
|
|
79
|
+
npm run test:coverage
|
|
80
|
+
|
|
81
|
+
# Run tests in watch mode
|
|
82
|
+
npm run test:watch
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Test Structure
|
|
86
|
+
|
|
87
|
+
- **Unit Tests** (`__tests__/unit/`): Test individual components
|
|
88
|
+
- `PluginManager.test.ts`: Plugin loading and management
|
|
89
|
+
- `EventBus.test.ts`: Event publishing and subscription
|
|
90
|
+
- `concepts/User.test.ts`: User concept business logic
|
|
91
|
+
|
|
92
|
+
- **Integration Tests** (`__tests__/integration/`): Test complete workflows
|
|
93
|
+
- `ecommerce-flow.test.ts`: End-to-end e-commerce scenarios
|
|
94
|
+
|
|
95
|
+
### Test Coverage
|
|
96
|
+
|
|
97
|
+
The test suite covers:
|
|
98
|
+
- ✅ Plugin loading and initialization
|
|
99
|
+
- ✅ Event publishing and subscription
|
|
100
|
+
- ✅ Business logic validation
|
|
101
|
+
- ✅ Cross-plugin communication
|
|
102
|
+
- ✅ Error handling
|
|
103
|
+
- ✅ Async operation handling
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { LegibleEngine } from '@legible-sync/core';
|
|
2
|
+
import { PluginManager } from '../../src/core/PluginManager';
|
|
3
|
+
import { EventBus } from '../../src/core/EventBus';
|
|
4
|
+
import { usersPlugin } from '../../src/plugins/users';
|
|
5
|
+
import { productsPlugin } from '../../src/plugins/products';
|
|
6
|
+
import { ordersPlugin } from '../../src/plugins/orders';
|
|
7
|
+
import { inventoryPlugin } from '../../src/plugins/inventory';
|
|
8
|
+
import { notificationsPlugin } from '../../src/plugins/notifications';
|
|
9
|
+
import { analyticsPlugin } from '../../src/plugins/analytics';
|
|
10
|
+
import { paymentsPlugin } from '../../src/plugins/payments';
|
|
11
|
+
|
|
12
|
+
describe('E-commerce Flow Integration', () => {
|
|
13
|
+
let engine: LegibleEngine;
|
|
14
|
+
let pluginManager: PluginManager;
|
|
15
|
+
let eventBus: EventBus;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
engine = new LegibleEngine();
|
|
19
|
+
pluginManager = new PluginManager(engine);
|
|
20
|
+
eventBus = new EventBus();
|
|
21
|
+
|
|
22
|
+
// Register EventBus as a concept
|
|
23
|
+
engine.registerConcept('EventBus', {
|
|
24
|
+
state: {},
|
|
25
|
+
async execute(action: string, input: any) {
|
|
26
|
+
if (action === 'publish') {
|
|
27
|
+
const { event, data } = input;
|
|
28
|
+
await eventBus.publish({
|
|
29
|
+
type: event,
|
|
30
|
+
payload: data,
|
|
31
|
+
source: 'system',
|
|
32
|
+
flowId: input.flowId || 'system'
|
|
33
|
+
});
|
|
34
|
+
return { published: true };
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Unknown EventBus action: ${action}`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Load all plugins
|
|
41
|
+
await pluginManager.loadPlugin(usersPlugin);
|
|
42
|
+
await pluginManager.loadPlugin(productsPlugin);
|
|
43
|
+
await pluginManager.loadPlugin(inventoryPlugin);
|
|
44
|
+
await pluginManager.loadPlugin(ordersPlugin);
|
|
45
|
+
await pluginManager.loadPlugin(paymentsPlugin);
|
|
46
|
+
await pluginManager.loadPlugin(notificationsPlugin);
|
|
47
|
+
await pluginManager.loadPlugin(analyticsPlugin);
|
|
48
|
+
|
|
49
|
+
// Reset concept states for test isolation
|
|
50
|
+
await engine.invoke('User', 'reset', {}, 'reset');
|
|
51
|
+
await engine.invoke('Product', 'reset', {}, 'reset');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('Complete User Registration Flow', () => {
|
|
55
|
+
it('should handle user registration with analytics tracking', async () => {
|
|
56
|
+
const flowId = 'user-reg-flow';
|
|
57
|
+
|
|
58
|
+
const result = await engine.invoke('User', 'register', {
|
|
59
|
+
username: 'johndoe',
|
|
60
|
+
email: 'john@example.com',
|
|
61
|
+
password: 'password123'
|
|
62
|
+
}, flowId);
|
|
63
|
+
|
|
64
|
+
expect(result.userId).toBeDefined();
|
|
65
|
+
expect(result.user.username).toBe('johndoe');
|
|
66
|
+
expect(result.user.email).toBe('john@example.com');
|
|
67
|
+
|
|
68
|
+
// Check analytics tracking
|
|
69
|
+
const analytics = await engine.invoke('Analytics', 'getMetrics', {}, 'analytics-check');
|
|
70
|
+
expect(analytics.metrics['user_registered']).toBe(1);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('Complete Product Management Flow', () => {
|
|
75
|
+
it('should handle product creation with analytics tracking', async () => {
|
|
76
|
+
const flowId = 'product-create-flow';
|
|
77
|
+
|
|
78
|
+
const result = await engine.invoke('Product', 'create', {
|
|
79
|
+
name: 'Test Product',
|
|
80
|
+
sku: 'TEST-001',
|
|
81
|
+
price: 29.99,
|
|
82
|
+
description: 'A test product',
|
|
83
|
+
category: 'test'
|
|
84
|
+
}, flowId);
|
|
85
|
+
|
|
86
|
+
expect(result.productId).toBeDefined();
|
|
87
|
+
expect(result.product.name).toBe('Test Product');
|
|
88
|
+
expect(result.product.sku).toBe('TEST-001');
|
|
89
|
+
|
|
90
|
+
// Check analytics tracking
|
|
91
|
+
const analytics = await engine.invoke('Analytics', 'getMetrics', {}, 'analytics-check');
|
|
92
|
+
expect(analytics.metrics['product_created']).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('Order Processing Flow', () => {
|
|
97
|
+
let userId: string;
|
|
98
|
+
let productId: string;
|
|
99
|
+
|
|
100
|
+
beforeEach(async () => {
|
|
101
|
+
// Create user
|
|
102
|
+
const userResult = await engine.invoke('User', 'register', {
|
|
103
|
+
username: 'testuser',
|
|
104
|
+
email: 'test@example.com',
|
|
105
|
+
password: 'password123'
|
|
106
|
+
}, 'setup-user');
|
|
107
|
+
|
|
108
|
+
userId = userResult.userId;
|
|
109
|
+
|
|
110
|
+
// Create product
|
|
111
|
+
const productResult = await engine.invoke('Product', 'create', {
|
|
112
|
+
name: 'Test Product',
|
|
113
|
+
sku: 'TEST-001',
|
|
114
|
+
price: 29.99,
|
|
115
|
+
category: 'test'
|
|
116
|
+
}, 'setup-product');
|
|
117
|
+
|
|
118
|
+
productId = productResult.productId;
|
|
119
|
+
|
|
120
|
+
// Set inventory
|
|
121
|
+
await engine.invoke('Inventory', 'setStock', {
|
|
122
|
+
productId,
|
|
123
|
+
quantity: 10
|
|
124
|
+
}, 'setup-inventory');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should process complete order workflow', async () => {
|
|
128
|
+
const orderFlow = 'order-flow';
|
|
129
|
+
|
|
130
|
+
// Create order
|
|
131
|
+
const orderResult = await engine.invoke('Order', 'create', {
|
|
132
|
+
userId,
|
|
133
|
+
items: [
|
|
134
|
+
{
|
|
135
|
+
productId,
|
|
136
|
+
quantity: 2,
|
|
137
|
+
price: 29.99
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}, orderFlow);
|
|
141
|
+
|
|
142
|
+
expect(orderResult.orderId).toBeDefined();
|
|
143
|
+
|
|
144
|
+
// Wait for async operations
|
|
145
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
146
|
+
|
|
147
|
+
// Check order status (should be auto-confirmed)
|
|
148
|
+
const orderStatus = await engine.invoke('Order', 'get', {
|
|
149
|
+
orderId: orderResult.orderId
|
|
150
|
+
}, 'status-check');
|
|
151
|
+
|
|
152
|
+
expect(orderStatus.order.status).toBe('confirmed');
|
|
153
|
+
expect(orderStatus.order.total).toBe(59.98); // 2 * 29.99
|
|
154
|
+
|
|
155
|
+
// Check inventory deduction
|
|
156
|
+
const inventoryStatus = await engine.invoke('Inventory', 'getStock', {
|
|
157
|
+
productId
|
|
158
|
+
}, 'inventory-check');
|
|
159
|
+
|
|
160
|
+
expect(inventoryStatus.quantity).toBe(8); // 10 - 2
|
|
161
|
+
|
|
162
|
+
// Check analytics
|
|
163
|
+
const analytics = await engine.invoke('Analytics', 'getMetrics', {}, 'analytics-check');
|
|
164
|
+
expect(analytics.metrics['order_created']).toBe(1);
|
|
165
|
+
expect(analytics.metrics['order_confirmed']).toBe(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should handle order confirmation notifications', async () => {
|
|
169
|
+
// Create and confirm order
|
|
170
|
+
const orderResult = await engine.invoke('Order', 'create', {
|
|
171
|
+
userId,
|
|
172
|
+
items: [{ productId, quantity: 1, price: 29.99 }]
|
|
173
|
+
}, 'notification-test');
|
|
174
|
+
|
|
175
|
+
// Wait for processing
|
|
176
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
177
|
+
|
|
178
|
+
// Check notification history
|
|
179
|
+
const notifications = await engine.invoke('Notification', 'getHistory', {
|
|
180
|
+
recipient: 'test@example.com'
|
|
181
|
+
}, 'notification-check');
|
|
182
|
+
|
|
183
|
+
expect(notifications.notifications.length).toBeGreaterThan(0);
|
|
184
|
+
const orderNotification = notifications.notifications.find((n: any) =>
|
|
185
|
+
n.template === 'order-confirmation'
|
|
186
|
+
);
|
|
187
|
+
expect(orderNotification).toBeDefined();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('Plugin Integration', () => {
|
|
192
|
+
it('should have all plugins loaded', () => {
|
|
193
|
+
const loadedPlugins = pluginManager.getLoadedPlugins();
|
|
194
|
+
expect(loadedPlugins).toHaveLength(7);
|
|
195
|
+
expect(loadedPlugins).toContain('users');
|
|
196
|
+
expect(loadedPlugins).toContain('products');
|
|
197
|
+
expect(loadedPlugins).toContain('orders');
|
|
198
|
+
expect(loadedPlugins).toContain('inventory');
|
|
199
|
+
expect(loadedPlugins).toContain('payments');
|
|
200
|
+
expect(loadedPlugins).toContain('notifications');
|
|
201
|
+
expect(loadedPlugins).toContain('analytics');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should handle cross-plugin communication', async () => {
|
|
205
|
+
// Create user
|
|
206
|
+
const userResult = await engine.invoke('User', 'register', {
|
|
207
|
+
username: 'integration-test',
|
|
208
|
+
email: 'integration@example.com',
|
|
209
|
+
password: 'password123'
|
|
210
|
+
}, 'integration-flow');
|
|
211
|
+
|
|
212
|
+
// Create product
|
|
213
|
+
const productResult = await engine.invoke('Product', 'create', {
|
|
214
|
+
name: 'Integration Product',
|
|
215
|
+
sku: 'INT-001',
|
|
216
|
+
price: 19.99,
|
|
217
|
+
category: 'integration'
|
|
218
|
+
}, 'integration-flow');
|
|
219
|
+
|
|
220
|
+
// Set inventory
|
|
221
|
+
await engine.invoke('Inventory', 'setStock', {
|
|
222
|
+
productId: productResult.productId,
|
|
223
|
+
quantity: 5
|
|
224
|
+
}, 'integration-flow');
|
|
225
|
+
|
|
226
|
+
// Create order (this triggers multiple plugins)
|
|
227
|
+
await engine.invoke('Order', 'create', {
|
|
228
|
+
userId: userResult.userId,
|
|
229
|
+
items: [{
|
|
230
|
+
productId: productResult.productId,
|
|
231
|
+
quantity: 1,
|
|
232
|
+
price: 19.99
|
|
233
|
+
}]
|
|
234
|
+
}, 'integration-flow');
|
|
235
|
+
|
|
236
|
+
// Wait for all async operations
|
|
237
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
238
|
+
|
|
239
|
+
// Verify cross-plugin effects
|
|
240
|
+
const analytics = await engine.invoke('Analytics', 'getMetrics', {}, 'final-check');
|
|
241
|
+
expect(analytics.metrics['user_registered']).toBeGreaterThanOrEqual(1);
|
|
242
|
+
expect(analytics.metrics['product_created']).toBeGreaterThanOrEqual(1);
|
|
243
|
+
expect(analytics.metrics['order_created']).toBeGreaterThanOrEqual(1);
|
|
244
|
+
expect(analytics.metrics['order_confirmed']).toBeGreaterThanOrEqual(1);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { EventBus, Event } from '../../src/core/EventBus';
|
|
2
|
+
|
|
3
|
+
describe('EventBus', () => {
|
|
4
|
+
let eventBus: EventBus;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
eventBus = new EventBus();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('Event Subscription', () => {
|
|
11
|
+
it('should subscribe to events', () => {
|
|
12
|
+
const handler = jest.fn();
|
|
13
|
+
eventBus.subscribe('user.created', handler);
|
|
14
|
+
|
|
15
|
+
// Should not throw, subscription is successful
|
|
16
|
+
expect(() => eventBus.subscribe('user.created', handler)).not.toThrow();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle multiple handlers for the same event', async () => {
|
|
20
|
+
const handler1 = jest.fn();
|
|
21
|
+
const handler2 = jest.fn();
|
|
22
|
+
|
|
23
|
+
eventBus.subscribe('order.placed', handler1);
|
|
24
|
+
eventBus.subscribe('order.placed', handler2);
|
|
25
|
+
|
|
26
|
+
const eventData = {
|
|
27
|
+
type: 'order.placed' as const,
|
|
28
|
+
payload: { orderId: '123' },
|
|
29
|
+
source: 'orders',
|
|
30
|
+
flowId: 'flow1'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
await eventBus.publish(eventData);
|
|
34
|
+
|
|
35
|
+
expect(handler1).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(handler2).toHaveBeenCalledTimes(1);
|
|
37
|
+
|
|
38
|
+
const calledEvent1 = handler1.mock.calls[0][0];
|
|
39
|
+
const calledEvent2 = handler2.mock.calls[0][0];
|
|
40
|
+
|
|
41
|
+
expect(calledEvent1.type).toBe('order.placed');
|
|
42
|
+
expect(calledEvent1.payload).toEqual({ orderId: '123' });
|
|
43
|
+
expect(calledEvent1.source).toBe('orders');
|
|
44
|
+
expect(calledEvent1.flowId).toBe('flow1');
|
|
45
|
+
expect(calledEvent1.timestamp).toBeInstanceOf(Date);
|
|
46
|
+
|
|
47
|
+
expect(calledEvent2.type).toBe('order.placed');
|
|
48
|
+
expect(calledEvent2.payload).toEqual({ orderId: '123' });
|
|
49
|
+
expect(calledEvent2.source).toBe('orders');
|
|
50
|
+
expect(calledEvent2.flowId).toBe('flow1');
|
|
51
|
+
expect(calledEvent2.timestamp).toBeInstanceOf(Date);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('Event Publishing', () => {
|
|
56
|
+
it('should publish events to subscribed handlers', async () => {
|
|
57
|
+
const handler = jest.fn();
|
|
58
|
+
eventBus.subscribe('product.updated', handler);
|
|
59
|
+
|
|
60
|
+
const eventData = {
|
|
61
|
+
type: 'product.updated' as const,
|
|
62
|
+
payload: { productId: '456', changes: { price: 29.99 } },
|
|
63
|
+
source: 'products',
|
|
64
|
+
flowId: 'flow2'
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
await eventBus.publish(eventData);
|
|
68
|
+
|
|
69
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
70
|
+
const calledEvent = handler.mock.calls[0][0];
|
|
71
|
+
expect(calledEvent.type).toBe('product.updated');
|
|
72
|
+
expect(calledEvent.payload).toEqual(eventData.payload);
|
|
73
|
+
expect(calledEvent.source).toBe('products');
|
|
74
|
+
expect(calledEvent.flowId).toBe('flow2');
|
|
75
|
+
expect(calledEvent.timestamp).toBeInstanceOf(Date);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should handle events with no subscribers', async () => {
|
|
79
|
+
const eventData = {
|
|
80
|
+
type: 'unknown.event' as const,
|
|
81
|
+
payload: { data: 'test' },
|
|
82
|
+
source: 'test',
|
|
83
|
+
flowId: 'flow3'
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Should not throw when publishing to non-existent subscribers
|
|
87
|
+
await expect(eventBus.publish(eventData)).resolves.not.toThrow();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle handler errors gracefully', async () => {
|
|
91
|
+
const goodHandler = jest.fn();
|
|
92
|
+
const badHandler = jest.fn().mockRejectedValue(new Error('Handler failed'));
|
|
93
|
+
|
|
94
|
+
eventBus.subscribe('test.event', goodHandler);
|
|
95
|
+
eventBus.subscribe('test.event', badHandler);
|
|
96
|
+
|
|
97
|
+
const eventData = {
|
|
98
|
+
type: 'test.event' as const,
|
|
99
|
+
payload: { test: true },
|
|
100
|
+
source: 'test',
|
|
101
|
+
flowId: 'flow4'
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Should not throw even if one handler fails
|
|
105
|
+
await expect(eventBus.publish(eventData)).resolves.not.toThrow();
|
|
106
|
+
|
|
107
|
+
// Good handler should still be called
|
|
108
|
+
expect(goodHandler).toHaveBeenCalledTimes(1);
|
|
109
|
+
expect(badHandler).toHaveBeenCalledTimes(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should call handlers asynchronously', async () => {
|
|
113
|
+
const handler = jest.fn().mockResolvedValue(undefined);
|
|
114
|
+
eventBus.subscribe('async.event', handler);
|
|
115
|
+
|
|
116
|
+
const eventData = {
|
|
117
|
+
type: 'async.event' as const,
|
|
118
|
+
payload: { async: true },
|
|
119
|
+
source: 'async',
|
|
120
|
+
flowId: 'flow5'
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const publishPromise = eventBus.publish(eventData);
|
|
124
|
+
|
|
125
|
+
// Should return a promise
|
|
126
|
+
expect(publishPromise).toBeInstanceOf(Promise);
|
|
127
|
+
|
|
128
|
+
await publishPromise;
|
|
129
|
+
|
|
130
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('Event Creation Helper', () => {
|
|
135
|
+
it('should create events from engine actions', () => {
|
|
136
|
+
const event = eventBus.createEventFromAction(
|
|
137
|
+
'User',
|
|
138
|
+
'register',
|
|
139
|
+
{ username: 'testuser' },
|
|
140
|
+
{ userId: '123', user: { id: '123' } },
|
|
141
|
+
'flow6'
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(event.type).toBe('User.register');
|
|
145
|
+
expect(event.payload).toEqual({
|
|
146
|
+
input: { username: 'testuser' },
|
|
147
|
+
output: { userId: '123', user: { id: '123' } }
|
|
148
|
+
});
|
|
149
|
+
expect(event.source).toBe('User');
|
|
150
|
+
expect(event.flowId).toBe('flow6');
|
|
151
|
+
expect(event.timestamp).toBeInstanceOf(Date);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { LegibleEngine } from '@legible-sync/core';
|
|
2
|
+
import { PluginManager, Plugin } from '../../src/core/PluginManager';
|
|
3
|
+
|
|
4
|
+
describe('PluginManager', () => {
|
|
5
|
+
let engine: LegibleEngine;
|
|
6
|
+
let pluginManager: PluginManager;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
engine = new LegibleEngine();
|
|
10
|
+
pluginManager = new PluginManager(engine);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('Plugin Loading', () => {
|
|
14
|
+
it('should load a plugin successfully', async () => {
|
|
15
|
+
const mockPlugin: Plugin = {
|
|
16
|
+
name: 'test-plugin',
|
|
17
|
+
concepts: {
|
|
18
|
+
TestConcept: {
|
|
19
|
+
state: { counter: 0 },
|
|
20
|
+
execute: jest.fn().mockResolvedValue({ success: true })
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
syncs: [],
|
|
24
|
+
initialize: jest.fn().mockResolvedValue(undefined)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
await pluginManager.loadPlugin(mockPlugin);
|
|
28
|
+
|
|
29
|
+
expect(mockPlugin.initialize).toHaveBeenCalledWith(engine);
|
|
30
|
+
expect(pluginManager.getLoadedPlugins()).toContain('test-plugin');
|
|
31
|
+
expect(pluginManager.getPlugin('test-plugin')).toBe(mockPlugin);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should register concepts and syncs with the engine', async () => {
|
|
35
|
+
const mockConcept = {
|
|
36
|
+
state: {},
|
|
37
|
+
execute: jest.fn().mockResolvedValue({ result: 'ok' })
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const mockSync = {
|
|
41
|
+
name: 'test-sync',
|
|
42
|
+
when: [{ concept: 'TestConcept', action: 'test' }],
|
|
43
|
+
then: [{ concept: 'OtherConcept', action: 'handle', input: {} }]
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const mockPlugin: Plugin = {
|
|
47
|
+
name: 'test-plugin',
|
|
48
|
+
concepts: { TestConcept: mockConcept },
|
|
49
|
+
syncs: [mockSync]
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Mock engine methods
|
|
53
|
+
const registerConceptSpy = jest.spyOn(engine as any, 'registerConcept');
|
|
54
|
+
const registerSyncSpy = jest.spyOn(engine as any, 'registerSync');
|
|
55
|
+
|
|
56
|
+
await pluginManager.loadPlugin(mockPlugin);
|
|
57
|
+
|
|
58
|
+
expect(registerConceptSpy).toHaveBeenCalledWith('TestConcept', mockConcept);
|
|
59
|
+
expect(registerSyncSpy).toHaveBeenCalledWith(mockSync);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle plugin initialization errors', async () => {
|
|
63
|
+
const mockPlugin: Plugin = {
|
|
64
|
+
name: 'failing-plugin',
|
|
65
|
+
concepts: {},
|
|
66
|
+
syncs: [],
|
|
67
|
+
initialize: jest.fn().mockRejectedValue(new Error('Init failed'))
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
await expect(pluginManager.loadPlugin(mockPlugin)).rejects.toThrow('Init failed');
|
|
71
|
+
expect(pluginManager.getLoadedPlugins()).not.toContain('failing-plugin');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('Plugin Management', () => {
|
|
76
|
+
it('should return loaded plugin names', async () => {
|
|
77
|
+
const plugin1: Plugin = {
|
|
78
|
+
name: 'plugin1',
|
|
79
|
+
concepts: {},
|
|
80
|
+
syncs: []
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const plugin2: Plugin = {
|
|
84
|
+
name: 'plugin2',
|
|
85
|
+
concepts: {},
|
|
86
|
+
syncs: []
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await pluginManager.loadPlugin(plugin1);
|
|
90
|
+
await pluginManager.loadPlugin(plugin2);
|
|
91
|
+
|
|
92
|
+
const loadedPlugins = pluginManager.getLoadedPlugins();
|
|
93
|
+
expect(loadedPlugins).toHaveLength(2);
|
|
94
|
+
expect(loadedPlugins).toContain('plugin1');
|
|
95
|
+
expect(loadedPlugins).toContain('plugin2');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return specific plugin by name', async () => {
|
|
99
|
+
const mockPlugin: Plugin = {
|
|
100
|
+
name: 'specific-plugin',
|
|
101
|
+
concepts: {},
|
|
102
|
+
syncs: []
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
await pluginManager.loadPlugin(mockPlugin);
|
|
106
|
+
|
|
107
|
+
expect(pluginManager.getPlugin('specific-plugin')).toBe(mockPlugin);
|
|
108
|
+
expect(pluginManager.getPlugin('non-existent')).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|