data-primals-engine 1.7.1 → 1.7.3
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 +9 -9
- package/client/package-lock.json +8430 -8121
- package/client/package.json +7 -4
- package/client/src/APIInfo.jsx +1 -1
- package/client/src/App.jsx +2 -1
- package/client/src/App.scss +1635 -1626
- package/client/src/AssistantChat.jsx +2 -3
- package/client/src/CalendarView.jsx +1 -0
- package/client/src/ContentView.jsx +3 -3
- package/client/src/Dashboard.jsx +5 -2
- package/client/src/DashboardChart.jsx +1 -0
- package/client/src/DashboardFlexViewItem.jsx +1 -0
- package/client/src/DashboardHtmlViewItem.jsx +1 -0
- package/client/src/DashboardView.jsx +2 -0
- package/client/src/DataEditor.jsx +0 -1
- package/client/src/DataImporter.jsx +489 -468
- package/client/src/DataLayout.jsx +25 -23
- package/client/src/DataTable.jsx +6 -5
- package/client/src/Dialog.jsx +92 -90
- package/client/src/Dialog.scss +122 -116
- package/client/src/DisplayFlexNodeRenderer.jsx +1 -0
- package/client/src/DocumentationPageLayout.scss +1 -1
- package/client/src/FlexBuilderControls.jsx +1 -0
- package/client/src/FlexBuilderPreview.jsx +1 -1
- package/client/src/FlexNode.jsx +1 -1
- package/client/src/HistoryDialog.jsx +3 -2
- package/client/src/KPIWidget.jsx +1 -1
- package/client/src/KanbanView.jsx +1 -0
- package/client/src/ModelCreator.jsx +4 -0
- package/client/src/ModelList.jsx +1 -0
- package/client/src/PackGallery.jsx +5 -4
- package/client/src/RTETrans.jsx +1 -1
- package/client/src/RelationField.jsx +2 -0
- package/client/src/RelationSelectorWidget.jsx +2 -0
- package/client/src/RelationValue.jsx +1 -0
- package/client/src/RestoreDialog.jsx +1 -0
- package/client/src/ViewSwitcher.jsx +1 -1
- package/client/src/WorkflowEditor.jsx +3 -0
- package/client/src/contexts/CommandContext.jsx +2 -1
- package/client/src/contexts/ModelContext.jsx +3 -3
- package/client/src/hooks/data.js +1 -0
- package/client/src/hooks/useValidation.js +1 -0
- package/client/src/translations.js +24 -24
- package/client/vite.config.js +31 -30
- package/doc/AI-assistance.md +87 -63
- package/doc/Concepts.md +122 -0
- package/doc/Custom-Endpoints.md +31 -0
- package/doc/Event-system.md +13 -14
- package/doc/Home.md +33 -0
- package/doc/Modules.md +83 -0
- package/doc/Packs-gallery.md +8 -23
- package/doc/Workflows.md +32 -0
- package/doc/automation-workflows.md +141 -102
- package/doc/dashboards-kpis-charts.md +47 -49
- package/doc/data-management.md +126 -120
- package/doc/data-models.md +68 -75
- package/doc/roles-permissions.md +144 -43
- package/doc/sharding-replication.md +158 -0
- package/doc/users.md +54 -30
- package/package.json +27 -17
- package/server.js +37 -37
- package/src/ai.jobs.js +135 -0
- package/src/constants.js +560 -545
- package/src/data.js +521 -518
- package/src/email.js +157 -154
- package/src/engine.js +167 -49
- package/src/gameObject.js +6 -0
- package/src/i18n.js +0 -1
- package/src/modules/assistant/assistant.js +782 -763
- package/src/modules/assistant/constants.js +23 -16
- package/src/modules/assistant/providers.js +77 -37
- package/src/modules/auth-google/index.js +53 -50
- package/src/modules/bucket.js +346 -335
- package/src/modules/data/data.backup.js +400 -376
- package/src/modules/data/data.cluster.js +559 -0
- package/src/modules/data/data.core.js +11 -8
- package/src/modules/data/data.js +311 -311
- package/src/modules/data/data.operations.js +3666 -3555
- package/src/modules/data/data.relations.js +1 -0
- package/src/modules/data/data.replication.js +125 -0
- package/src/modules/data/data.routes.js +2206 -1879
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/file.js +248 -247
- package/src/modules/mongodb.js +76 -73
- package/src/modules/user.js +18 -2
- package/src/modules/worker-script-runner.js +97 -0
- package/src/modules/workflow.js +177 -52
- package/src/packs.js +5701 -5701
- package/src/providers.js +298 -297
- package/src/sso.js +91 -25
- package/test/assistant.test.js +207 -206
- package/test/cluster.test.js +221 -0
- package/test/core.test.js +0 -2
- package/test/data.integration.test.js +1425 -1416
- package/test/import_export.integration.test.js +210 -210
- package/test/replication.test.js +163 -0
- package/test/workflow.actions.integration.test.js +487 -475
- package/test/workflow.integration.test.js +332 -329
- package/doc/core-concepts.md +0 -33
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { initializeCluster, getMemberList, stopClusterServices } from '../src/modules/data/data.cluster.js';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import { Logger } from "../src/index.js";
|
|
5
|
+
import { Config } from '../src/config.js';
|
|
6
|
+
|
|
7
|
+
// Mock Logger to prevent console output
|
|
8
|
+
vi.mock('../src/gameObject.js', async (importOriginal) => {
|
|
9
|
+
const original = await importOriginal();
|
|
10
|
+
return {
|
|
11
|
+
...original, // Keep original exports like 'Behaviour'
|
|
12
|
+
Logger: class {
|
|
13
|
+
info = vi.fn();
|
|
14
|
+
warn = vi.fn();
|
|
15
|
+
error = vi.fn();
|
|
16
|
+
debug = vi.fn();
|
|
17
|
+
},
|
|
18
|
+
DataHandler: class {} // Provide a mock DataHandler class
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Mock Config
|
|
23
|
+
vi.mock('../src/config.js', () => ({
|
|
24
|
+
Config: {
|
|
25
|
+
Get: vi.fn((key, defaultValue) => defaultValue)
|
|
26
|
+
}
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// Mock fs.promises pour contrôler statfs
|
|
30
|
+
vi.mock('node:fs/promises', async (importOriginal) => {
|
|
31
|
+
const actual = await importOriginal();
|
|
32
|
+
return {
|
|
33
|
+
...actual,
|
|
34
|
+
statfs: vi.fn().mockResolvedValue({ bavail: 5000, bsize: 1024, blocks: 10000 }), // Mock de base
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Data Cluster & Gossip Logic', () => {
|
|
39
|
+
let mockEngine;
|
|
40
|
+
let postHandlers = {};
|
|
41
|
+
let mockDataHandler;
|
|
42
|
+
let mockClusterLeaseModel;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.useFakeTimers();
|
|
46
|
+
|
|
47
|
+
// Mock the engine
|
|
48
|
+
mockEngine = {
|
|
49
|
+
selfUrl: 'http://node-1:3000',
|
|
50
|
+
peers: [
|
|
51
|
+
{ id: 'node-1', public_domain: 'http://node-1:3000', sharding: true, replica: true },
|
|
52
|
+
{ id: 'node-2', public_domain: 'http://node-2:3000', sharding: true, replica: true },
|
|
53
|
+
{ id: 'node-3', public_domain: 'http://node-3:3000', sharding: true, replica: true }
|
|
54
|
+
],
|
|
55
|
+
getComponent: vi.fn(componentName => {
|
|
56
|
+
if (componentName === 'DataHandler') return mockDataHandler;
|
|
57
|
+
return new Logger('mock');
|
|
58
|
+
}),
|
|
59
|
+
sendToPeer: vi.fn(),
|
|
60
|
+
post: vi.fn((path, handler) => {
|
|
61
|
+
postHandlers[path] = handler;
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Mock Mongoose/DataHandler for leasing
|
|
66
|
+
mockClusterLeaseModel = {
|
|
67
|
+
findOneAndUpdate: vi.fn(),
|
|
68
|
+
};
|
|
69
|
+
mockDataHandler = {
|
|
70
|
+
mongoose: {
|
|
71
|
+
Schema: class Schema {},
|
|
72
|
+
model: vi.fn().mockReturnValue(mockClusterLeaseModel),
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Reset post handlers for each test
|
|
77
|
+
postHandlers = {};
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
stopClusterServices();
|
|
82
|
+
vi.restoreAllMocks();
|
|
83
|
+
vi.useRealTimers();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('Initialization (onInit)', () => {
|
|
87
|
+
it('should initialize member list from static peer configuration', async () => {
|
|
88
|
+
await initializeCluster(mockEngine);
|
|
89
|
+
const memberList = getMemberList();
|
|
90
|
+
|
|
91
|
+
expect(memberList).toHaveLength(3);
|
|
92
|
+
expect(memberList.find(m => m.id === 'node-1')).toBeDefined();
|
|
93
|
+
expect(memberList.find(m => m.id === 'node-2')).toBeDefined();
|
|
94
|
+
expect(memberList.find(m => m.id === 'node-3')).toBeDefined();
|
|
95
|
+
|
|
96
|
+
const node1 = memberList.find(m => m.id === 'node-1');
|
|
97
|
+
expect(node1.status).toBe('UP');
|
|
98
|
+
expect(node1.version).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should correctly identify selfId', async () => {
|
|
102
|
+
await initializeCluster(mockEngine);
|
|
103
|
+
expect(mockEngine.selfId).toBe('node-1');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should register the /api/internal/gossip endpoint', async () => {
|
|
107
|
+
await initializeCluster(mockEngine);
|
|
108
|
+
expect(mockEngine.post).toHaveBeenCalledWith('/api/internal/gossip', expect.any(Function));
|
|
109
|
+
expect(postHandlers['/api/internal/gossip']).toBeDefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should start the gossip interval', async () => {
|
|
113
|
+
const setIntervalSpy = vi.spyOn(global, 'setInterval');
|
|
114
|
+
await initializeCluster(mockEngine);
|
|
115
|
+
expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 2000); // Default interval
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('Gossip Execution', () => {
|
|
120
|
+
it('should send its member list to a random UP peer', async () => {
|
|
121
|
+
await initializeCluster(mockEngine);
|
|
122
|
+
mockEngine.sendToPeer.mockResolvedValue({ ok: true, json: () => Promise.resolve([]) });
|
|
123
|
+
|
|
124
|
+
await vi.advanceTimersByTimeAsync(2000); // Trigger gossip
|
|
125
|
+
|
|
126
|
+
expect(mockEngine.sendToPeer).toHaveBeenCalledTimes(1);
|
|
127
|
+
const [peerId, path, payload] = mockEngine.sendToPeer.mock.calls[0];
|
|
128
|
+
expect(['node-2', 'node-3']).toContain(peerId);
|
|
129
|
+
expect(path).toBe('/api/internal/gossip');
|
|
130
|
+
expect(payload).toHaveLength(3);
|
|
131
|
+
expect(payload.find(p => p.id === 'node-1')).toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should mark a peer as SUSPECT if sendToPeer fails', async () => {
|
|
135
|
+
await initializeCluster(mockEngine);
|
|
136
|
+
mockEngine.sendToPeer.mockRejectedValue(new Error('Network error'));
|
|
137
|
+
|
|
138
|
+
await vi.advanceTimersByTimeAsync(2000); // Trigger gossip
|
|
139
|
+
|
|
140
|
+
expect(mockEngine.sendToPeer).toHaveBeenCalledTimes(1);
|
|
141
|
+
const failedPeerId = mockEngine.sendToPeer.mock.calls[0][0];
|
|
142
|
+
|
|
143
|
+
const memberList = getMemberList();
|
|
144
|
+
const failedPeer = memberList.find(m => m.id === failedPeerId);
|
|
145
|
+
expect(failedPeer.status).toBe('SUSPECT');
|
|
146
|
+
expect(failedPeer.version).toBe(2); // Version incremented
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should merge the list received from a peer on successful gossip', async () => {
|
|
150
|
+
const remoteList = [
|
|
151
|
+
{ id: 'node-1', public_domain: 'http://node-1:3000', status: 'UP', version: 1 },
|
|
152
|
+
{ id: 'node-2', public_domain: 'http://node-2:3000', status: 'UP', version: 2 }, // Higher version
|
|
153
|
+
{ id: 'node-4', public_domain: 'http://node-4:3000', status: 'UP', version: 1 } // New node
|
|
154
|
+
];
|
|
155
|
+
mockEngine.sendToPeer.mockResolvedValue({ ok: true, json: () => Promise.resolve(remoteList) });
|
|
156
|
+
await initializeCluster(mockEngine);
|
|
157
|
+
|
|
158
|
+
await vi.advanceTimersByTimeAsync(2000); // Trigger gossip
|
|
159
|
+
|
|
160
|
+
const memberList = getMemberList();
|
|
161
|
+
expect(memberList).toHaveLength(4);
|
|
162
|
+
expect(memberList.find(m => m.id === 'node-4')).toBeDefined(); // New node added
|
|
163
|
+
expect(memberList.find(m => m.id === 'node-2').version).toBe(2); // Node updated
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('Gossip Health Checks', () => {
|
|
168
|
+
it('should mark a SUSPECT peer as DOWN after a timeout', async () => {
|
|
169
|
+
Config.Get.mockImplementation((key, defaultValue) => {
|
|
170
|
+
if (key === 'gossipSuspectTimeout') return 5000; // 5s timeout for test
|
|
171
|
+
return defaultValue;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Rendre le choix du pair déterministe en mockant Math.random
|
|
175
|
+
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0);
|
|
176
|
+
|
|
177
|
+
await initializeCluster(mockEngine);
|
|
178
|
+
mockEngine.sendToPeer.mockRejectedValue(new Error('Network error'));
|
|
179
|
+
|
|
180
|
+
// Le premier gossip échoue, node-2 devient SUSPECT
|
|
181
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
182
|
+
const memberListV1 = getMemberList();
|
|
183
|
+
expect(memberListV1.find(m => m.id === 'node-2')?.status).toBe('SUSPECT');
|
|
184
|
+
|
|
185
|
+
// On avance le temps au-delà du timeout + un autre cycle de gossip pour que checkSuspectNodes s'exécute.
|
|
186
|
+
await vi.advanceTimersByTimeAsync(8000); // 6000ms (timeout) + 2000ms (next gossip)
|
|
187
|
+
|
|
188
|
+
const memberListV2 = getMemberList();
|
|
189
|
+
expect(memberListV2.find(m => m.id === 'node-2')?.status).toBe('DOWN');
|
|
190
|
+
|
|
191
|
+
randomSpy.mockRestore(); // Nettoyer le spy
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Gossip Endpoint', () => {
|
|
196
|
+
it('should merge received list and respond with its own updated list', async () => {
|
|
197
|
+
await initializeCluster(mockEngine);
|
|
198
|
+
const gossipEndpointHandler = postHandlers['/api/internal/gossip'];
|
|
199
|
+
|
|
200
|
+
const remoteList = [
|
|
201
|
+
{ id: 'node-3', public_domain: 'http://node-3:3000', status: 'SUSPECT', version: 5 },
|
|
202
|
+
{ id: 'node-4', public_domain: 'http://node-4:3000', status: 'UP', version: 1 }
|
|
203
|
+
];
|
|
204
|
+
const mockReq = { fields: remoteList };
|
|
205
|
+
const mockRes = { json: vi.fn() };
|
|
206
|
+
|
|
207
|
+
gossipEndpointHandler(mockReq, mockRes);
|
|
208
|
+
|
|
209
|
+
// Check if list was merged
|
|
210
|
+
const memberList = getMemberList();
|
|
211
|
+
expect(memberList).toHaveLength(4);
|
|
212
|
+
expect(memberList.find(m => m.id === 'node-4')).toBeDefined();
|
|
213
|
+
expect(memberList.find(m => m.id === 'node-3').status).toBe('SUSPECT');
|
|
214
|
+
expect(memberList.find(m => m.id === 'node-3').version).toBe(5);
|
|
215
|
+
|
|
216
|
+
// Check if it responded with the new list
|
|
217
|
+
expect(mockRes.json).toHaveBeenCalledWith(memberList);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
});
|