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.
Files changed (99) hide show
  1. package/README.md +9 -9
  2. package/client/package-lock.json +8430 -8121
  3. package/client/package.json +7 -4
  4. package/client/src/APIInfo.jsx +1 -1
  5. package/client/src/App.jsx +2 -1
  6. package/client/src/App.scss +1635 -1626
  7. package/client/src/AssistantChat.jsx +2 -3
  8. package/client/src/CalendarView.jsx +1 -0
  9. package/client/src/ContentView.jsx +3 -3
  10. package/client/src/Dashboard.jsx +5 -2
  11. package/client/src/DashboardChart.jsx +1 -0
  12. package/client/src/DashboardFlexViewItem.jsx +1 -0
  13. package/client/src/DashboardHtmlViewItem.jsx +1 -0
  14. package/client/src/DashboardView.jsx +2 -0
  15. package/client/src/DataEditor.jsx +0 -1
  16. package/client/src/DataImporter.jsx +489 -468
  17. package/client/src/DataLayout.jsx +25 -23
  18. package/client/src/DataTable.jsx +6 -5
  19. package/client/src/Dialog.jsx +92 -90
  20. package/client/src/Dialog.scss +122 -116
  21. package/client/src/DisplayFlexNodeRenderer.jsx +1 -0
  22. package/client/src/DocumentationPageLayout.scss +1 -1
  23. package/client/src/FlexBuilderControls.jsx +1 -0
  24. package/client/src/FlexBuilderPreview.jsx +1 -1
  25. package/client/src/FlexNode.jsx +1 -1
  26. package/client/src/HistoryDialog.jsx +3 -2
  27. package/client/src/KPIWidget.jsx +1 -1
  28. package/client/src/KanbanView.jsx +1 -0
  29. package/client/src/ModelCreator.jsx +4 -0
  30. package/client/src/ModelList.jsx +1 -0
  31. package/client/src/PackGallery.jsx +5 -4
  32. package/client/src/RTETrans.jsx +1 -1
  33. package/client/src/RelationField.jsx +2 -0
  34. package/client/src/RelationSelectorWidget.jsx +2 -0
  35. package/client/src/RelationValue.jsx +1 -0
  36. package/client/src/RestoreDialog.jsx +1 -0
  37. package/client/src/ViewSwitcher.jsx +1 -1
  38. package/client/src/WorkflowEditor.jsx +3 -0
  39. package/client/src/contexts/CommandContext.jsx +2 -1
  40. package/client/src/contexts/ModelContext.jsx +3 -3
  41. package/client/src/hooks/data.js +1 -0
  42. package/client/src/hooks/useValidation.js +1 -0
  43. package/client/src/translations.js +24 -24
  44. package/client/vite.config.js +31 -30
  45. package/doc/AI-assistance.md +87 -63
  46. package/doc/Concepts.md +122 -0
  47. package/doc/Custom-Endpoints.md +31 -0
  48. package/doc/Event-system.md +13 -14
  49. package/doc/Home.md +33 -0
  50. package/doc/Modules.md +83 -0
  51. package/doc/Packs-gallery.md +8 -23
  52. package/doc/Workflows.md +32 -0
  53. package/doc/automation-workflows.md +141 -102
  54. package/doc/dashboards-kpis-charts.md +47 -49
  55. package/doc/data-management.md +126 -120
  56. package/doc/data-models.md +68 -75
  57. package/doc/roles-permissions.md +144 -43
  58. package/doc/sharding-replication.md +158 -0
  59. package/doc/users.md +54 -30
  60. package/package.json +27 -17
  61. package/server.js +37 -37
  62. package/src/ai.jobs.js +135 -0
  63. package/src/constants.js +560 -545
  64. package/src/data.js +521 -518
  65. package/src/email.js +157 -154
  66. package/src/engine.js +167 -49
  67. package/src/gameObject.js +6 -0
  68. package/src/i18n.js +0 -1
  69. package/src/modules/assistant/assistant.js +782 -763
  70. package/src/modules/assistant/constants.js +23 -16
  71. package/src/modules/assistant/providers.js +77 -37
  72. package/src/modules/auth-google/index.js +53 -50
  73. package/src/modules/bucket.js +346 -335
  74. package/src/modules/data/data.backup.js +400 -376
  75. package/src/modules/data/data.cluster.js +559 -0
  76. package/src/modules/data/data.core.js +11 -8
  77. package/src/modules/data/data.js +311 -311
  78. package/src/modules/data/data.operations.js +3666 -3555
  79. package/src/modules/data/data.relations.js +1 -0
  80. package/src/modules/data/data.replication.js +125 -0
  81. package/src/modules/data/data.routes.js +2206 -1879
  82. package/src/modules/data/data.scheduling.js +2 -1
  83. package/src/modules/file.js +248 -247
  84. package/src/modules/mongodb.js +76 -73
  85. package/src/modules/user.js +18 -2
  86. package/src/modules/worker-script-runner.js +97 -0
  87. package/src/modules/workflow.js +177 -52
  88. package/src/packs.js +5701 -5701
  89. package/src/providers.js +298 -297
  90. package/src/sso.js +91 -25
  91. package/test/assistant.test.js +207 -206
  92. package/test/cluster.test.js +221 -0
  93. package/test/core.test.js +0 -2
  94. package/test/data.integration.test.js +1425 -1416
  95. package/test/import_export.integration.test.js +210 -210
  96. package/test/replication.test.js +163 -0
  97. package/test/workflow.actions.integration.test.js +487 -475
  98. package/test/workflow.integration.test.js +332 -329
  99. 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
+ });
package/test/core.test.js CHANGED
@@ -1,6 +1,4 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
2
  import {
5
3
  sleep,
6
4
  escapeRegex,