data-primals-engine 1.7.0 → 1.7.2

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 (39) hide show
  1. package/README.md +160 -160
  2. package/client/package-lock.json +1080 -212
  3. package/client/package.json +12 -6
  4. package/client/src/AssistantChat.jsx +1 -3
  5. package/client/src/DataLayout.jsx +19 -20
  6. package/client/src/DataTable.jsx +2 -2
  7. package/client/src/DocumentationPageLayout.scss +1 -1
  8. package/client/src/ViewSwitcher.jsx +1 -1
  9. package/client/vite.config.js +31 -30
  10. package/package.json +39 -23
  11. package/src/ai.jobs.js +135 -0
  12. package/src/constants.js +561 -545
  13. package/src/core.js +487 -477
  14. package/src/data.js +2 -0
  15. package/src/email.js +0 -2
  16. package/src/engine.js +50 -42
  17. package/src/filter.js +348 -343
  18. package/src/modules/assistant/assistant.js +782 -763
  19. package/src/modules/assistant/constants.js +23 -16
  20. package/src/modules/assistant/providers.js +77 -37
  21. package/src/modules/bucket.js +4 -0
  22. package/src/modules/data/data.cluster.js +191 -0
  23. package/src/modules/data/data.core.js +11 -8
  24. package/src/modules/data/data.js +13 -4
  25. package/src/modules/data/data.operations.js +186 -106
  26. package/src/modules/data/data.relations.js +1 -0
  27. package/src/modules/data/data.replication.js +83 -0
  28. package/src/modules/data/data.routes.js +2183 -1879
  29. package/src/modules/mongodb.js +76 -73
  30. package/src/modules/user.js +7 -1
  31. package/src/modules/worker-script-runner.js +97 -0
  32. package/src/modules/workflow.js +1953 -1815
  33. package/src/packs.js +5701 -5697
  34. package/src/providers.js +298 -297
  35. package/test/assistant.test.js +207 -206
  36. package/test/data.integration.test.js +1425 -1416
  37. package/test/import_export.integration.test.js +210 -210
  38. package/test/workflow.actions.integration.test.js +487 -475
  39. package/test/workflow.integration.test.js +332 -329
@@ -1,329 +1,332 @@
1
- import { expect, describe, it, beforeEach,afterEach, beforeAll, afterAll, vi } from 'vitest';
2
- import { Config } from "../src/config";
3
-
4
- import {insertData, editData, deleteData, patchData} from '../src/index.js';
5
- import { modelsCollection as getAppModelsCollection, getCollectionForUser } from '../src/modules/mongodb.js';
6
- import * as workflowModule from '../src/modules/workflow.js';
7
- import {initEngine} from "../src/setenv.js";
8
- import {purgeData} from "../src/modules/data/data.history.js";
9
-
10
- beforeAll(async () =>{
11
- Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
12
- await initEngine();
13
- })
14
- vi.mock('../src/modules/workflow.js', { spy: true })
15
- // --- Données Mock pour les tests ---
16
- const mockUser = {
17
- username: 'testuserWorkflow',
18
- _user: 'testuserWorkflow',
19
- userPlan: 'premium',
20
- email: 'testWorkflow@example.com'
21
- };
22
-
23
- // --- Définitions des modèles ---
24
-
25
- // Le modèle de données qui déclenchera les workflows
26
- const targetDataModel = {
27
- name: 'project',
28
- _user: mockUser.username,
29
- description: 'Un modèle pour les projets qui déclencheront des workflows',
30
- fields: [
31
- { name: 'projectName', type: 'string', required: true },
32
- { name: 'status', type: 'string', required: true }, // ex: 'new', 'active', 'archived'
33
- { name: 'budget', type: 'number' }
34
- ]
35
- };
36
-
37
- // Les modèles qui définissent le système de workflow lui-même.
38
- // Pour les tests, nous devons insérer leurs définitions dans la collection 'models',
39
- // puis leurs instances (ex: un trigger spécifique) dans la collection 'datas' de l'utilisateur.
40
- // Les modèles qui définissent le système de workflow lui-me.
41
- // Pour les tests, nous devons insérer leurs définitions dans la collection 'models',
42
- // puis leurs instances (ex: un trigger spécifique) dans la collection 'datas' de l'utilisateur.
43
- const workflowMetaModels = [
44
- {
45
- name: 'workflow',
46
- _user: mockUser.username,
47
- description:'',
48
- fields: [
49
- { name: 'name', type: 'string' },
50
- { name: 'startStep', type: 'relation', relation: 'workflowStep' }
51
- ]
52
- },
53
- {
54
- name: 'workflowStep',
55
- _user: mockUser.username,
56
- description:'',
57
- fields: [
58
- { name: 'name', type: 'string' },
59
- { name: 'onSuccessStep', type: 'relation', relation: 'workflowStep' },
60
- { name: 'onFailureStep', type: 'relation', relation: 'workflowStep' },
61
- { name: 'isTerminal', type: 'boolean' },
62
- { name: 'actions', type: 'array', itemsType: 'relation', relation: 'workflowAction' },
63
- { name: 'conditions', type: 'object' }
64
- ]
65
- },
66
- {
67
- name: 'workflowTrigger',
68
- _user: mockUser.username,
69
- description:'',
70
- fields: [
71
- { name: 'name', type: 'string' },
72
- { name: 'targetModel', type: 'model' },
73
- { name: 'onEvent', type: 'enum', items: ['DataAdded', 'DataEdited', 'DataDeleted', 'ModelAdded', 'ModelEdited', 'ModelDeleted'] },
74
- { name: 'isActive', type: 'boolean' },
75
- { name: 'dataFilter', type: 'object' },
76
- { name: 'workflow', type: 'relation', relation: 'workflow' },
77
- { name: 'cronExpression', type: 'cronSchedule' },
78
- { name: 'lockDurationMinutes', type: 'number' }
79
- ]
80
- },
81
- {
82
- name: 'workflowRun',
83
- _user: mockUser.username,
84
- description:'',
85
- fields: [
86
- { name: 'status', type: 'enum', items: ['pending', 'running', 'completed', 'failed', 'cancelled'] },
87
- { name: 'workflow', type: 'relation', relation: 'workflow' },
88
- { name: 'contextData', type: 'object' },
89
- { name: 'currentStep', type: 'relation', relation: 'workflowStep' },
90
- { name: 'error', type: 'string' },
91
- { name: 'startedAt', type: 'datetime' },
92
- { name: 'completedAt', type: 'datetime' },
93
- { name: 'stepExecutionsCount', type: 'object' }
94
- ]
95
- },
96
- {
97
- name: 'workflowAction',
98
- _user: mockUser.username,
99
- description:'',
100
- fields: [
101
- // Common
102
- { name: 'name', type: 'string' },
103
- { name: 'type', type: 'enum', items: ['Webhook', 'CreateData', 'UpdateData', 'DeleteData', 'GenerateAIContent', 'SendEmail'] },
104
- // Webhook
105
- { name: 'url', type: 'url' },
106
- { name: 'method', type: 'enum', items: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] },
107
- { name: 'headers', type: 'code', language: 'json' },
108
- { name: 'body', type: 'code', language: 'json' },
109
- // Create/Update/Delete
110
- { name: 'targetModel', type: 'model' },
111
- { name: 'dataToCreate', type: 'code', language: 'json' },
112
- { name: 'targetSelector', type: 'code', language: 'json' },
113
- { name: 'fieldsToUpdate', type: 'code', language: 'json' },
114
- { name: 'updateMultiple', type: 'boolean' },
115
- { name: 'deleteMultiple', type: 'boolean' },
116
- // GenerateAIContent
117
- { name: 'aiProvider', type: 'enum', items: ['OpenAI', 'Google', 'Anthropic', 'DeepSeek'] },
118
- { name: 'aiModel', type: 'string' },
119
- { name: 'prompt', type: 'richtext' },
120
- // SendEmail
121
- { name: 'emailRecipients', type: 'array', itemsType: 'string' },
122
- { name: 'emailSubject', type: 'string' },
123
- { name: 'emailContent', type: 'richtext' }
124
- ]
125
- }
126
- ];
127
-
128
-
129
- let testModelsColInstance;
130
- let testDatasColInstance;
131
-
132
- // On "espionne" `c` pour vérifier qu'elle est appelée sans l'exécuter réellement.
133
- // Cela nous permet de tester uniquement la logique de déclenchement.
134
- const processWorkflowRunSpy = vi.spyOn(workflowModule, 'processWorkflowRun');
135
-
136
- beforeEach(async () => {
137
- testModelsColInstance = getAppModelsCollection;
138
- testDatasColInstance = await getCollectionForUser(mockUser);
139
-
140
- // Réinitialiser l'espion
141
- processWorkflowRunSpy.mockClear();
142
-
143
- const mods = await testModelsColInstance.find({ $and: [{ _user: mockUser.username}, { $or: [{name: targetDataModel.name}, ...workflowMetaModels.map(m =>({name: m.name}))]}]}).toArray();
144
- if( mods.length === 0 ) {
145
- // Insérer les définitions de modèles nécessaires
146
- await testModelsColInstance.insertMany([
147
- {...targetDataModel},
148
- ...workflowMetaModels.map(m => ({...m})) // Copie pour éviter les mutations
149
- ]);
150
- }
151
- // tell vitest we use mocked time
152
- vi.useFakeTimers({ shouldAdvanceTime: true })
153
- })
154
-
155
- afterEach(() => {
156
- vi.runOnlyPendingTimers();
157
- // restoring date after each test run
158
- vi.useRealTimers()
159
- })
160
- afterAll(async () => {
161
- await purgeData(mockUser);
162
- const coll = await getCollectionForUser(mockUser);
163
- await coll.drop();
164
- })
165
- describe('Intégration des Workflows - triggerWorkflows', () => {
166
-
167
- let testWorkflow;
168
- let testStep;
169
-
170
- // Avant chaque test de ce bloc, on crée un workflow et une étape de base
171
- const initTest = async () => {
172
- // Nettoyer les données avant chaque test
173
- await testDatasColInstance.deleteMany({_user: "testuserWorkflow"});
174
-
175
- const workflowInsertResult = await insertData('workflow', { name: 'Test Workflow' }, {}, mockUser, false);
176
- testWorkflow = { _id: workflowInsertResult.insertedIds[0] };
177
-
178
- const stepInsertResult = await insertData('workflowStep', { name: 'Start Step', isTerminal: true }, {}, mockUser, false);
179
- testStep = { _id: stepInsertResult.insertedIds[0] };
180
-
181
- // Lier l'étape au workflow
182
- await editData('workflow', testWorkflow._id, { startStep: testStep._id.toString() }, {}, mockUser, false);
183
- };
184
-
185
- it('devrait créer un workflowRun lors de l\'ajout de données correspondant à un trigger "DataAdded"', async () => {
186
- await initTest();
187
- // 1. Arrange: Créer un déclencheur actif pour 'DataAdded' sur le modèle 'project'
188
- await insertData('workflowTrigger', {
189
- name: 'Trigger on Project Add',
190
- targetModel: targetDataModel.name,
191
- onEvent: 'DataAdded',
192
- isActive: true,
193
- workflow: testWorkflow._id.toString()
194
- }, {}, mockUser, false);
195
-
196
- // 2. Act: Insérer une donnée qui doit déclencher le workflow
197
- const projectData = { projectName: 'New Corp Website', status: 'new', budget: 50000 };
198
- const insertResult = await insertData(targetDataModel.name, projectData, {}, mockUser, true, true);
199
-
200
- expect(insertResult.success).toBe(true);
201
-
202
- const workflowRun = await testDatasColInstance.findOne({ _model: 'workflowRun' });
203
-
204
- expect(workflowRun).not.toBeNull();
205
- expect(workflowRun.status).toBe('completed');
206
- expect(workflowRun.workflow.toString()).toBe(testWorkflow._id.toString());
207
- expect(workflowRun.contextData.triggerData._id.toString()).toBe(insertResult.insertedIds[0].toString());
208
- expect(workflowRun.contextData.triggerData.projectName).toBe('New Corp Website');
209
-
210
- // --- Vérification du champ 'history' ---
211
- expect(workflowRun.history).toBeDefined();
212
- expect(Array.isArray(workflowRun.history)).toBe(true);
213
- expect(workflowRun.history.length).toBeGreaterThan(0);
214
- const firstStepHistory = workflowRun.history[0];
215
- expect(firstStepHistory.stepId).toBe(testStep._id.toString());
216
- expect(firstStepHistory.status).toBe('success');
217
-
218
- });
219
-
220
- it('ne devrait PAS créer de workflowRun si le trigger est inactif', async () => {
221
- await initTest();
222
- // 1. Arrange: Créer un déclencheur INACTIF
223
- await insertData('workflowTrigger', {
224
- name: 'Inactive Trigger',
225
- targetModel: targetDataModel.name,
226
- onEvent: 'DataAdded',
227
- isActive: false, // Inactif
228
- workflow: testWorkflow._id.toString()
229
- }, {}, mockUser, false);
230
-
231
- // 2. Act: Insérer des données
232
- await insertData(targetDataModel.name, { projectName: 'Secret Project', status: 'new' }, {}, mockUser, true);
233
-
234
- // 3. Assert: Aucun `workflowRun` ne doit être créé
235
- const workflowRunCount = await testDatasColInstance.countDocuments({ _model: 'workflowRun' });
236
- expect(workflowRunCount).toBe(0);
237
- });
238
-
239
- it('devrait créer un workflowRun si le dataFilter est satisfait', async () => {
240
- await initTest();
241
- // 1. Arrange: Créer un déclencheur avec un `dataFilter`
242
- await insertData('workflowTrigger', {
243
- name: 'Trigger for Active Projects',
244
- targetModel: targetDataModel.name,
245
- onEvent: 'DataAdded',
246
- isActive: true,
247
- dataFilter: { $eq: ['$status', 'active'] }, // Filtre pour status = 'active'
248
- workflow: testWorkflow._id.toString()
249
- }, {}, mockUser, false);
250
-
251
- // 2. Act: Insérer des données qui correspondent au filtre
252
- await insertData(targetDataModel.name, { projectName: 'Go-Live Project', status: 'active' }, {}, mockUser, true, true);
253
-
254
- // 3. Assert: Un `workflowRun` doit être créé
255
- const workflowRunCount = await testDatasColInstance.countDocuments({ _model: 'workflowRun' });
256
- expect(workflowRunCount).toBe(1);
257
- });
258
-
259
- it('ne devrait PAS créer de workflowRun si le dataFilter n\'est PAS satisfait', async () => {
260
- await initTest();
261
- // 1. Arrange: Créer un déclencheur avec un `dataFilter`
262
- await insertData('workflowTrigger', {
263
- name: 'Trigger for Archived Projects',
264
- targetModel: targetDataModel.name,
265
- onEvent: 'DataAdded',
266
- isActive: true,
267
- dataFilter: { $eq: ['$status', 'archived'] }, // Filtre pour status = 'archived'
268
- workflow: testWorkflow._id.toString()
269
- }, {}, mockUser, false);
270
-
271
- // 2. Act: Insérer des données qui NE correspondent PAS au filtre
272
- await insertData(targetDataModel.name, { projectName: 'Ongoing Project', status: 'active' }, {}, mockUser, true, true);
273
-
274
- // 3. Assert: Aucun `workflowRun` ne doit être créé
275
- const workflowRunCount = await testDatasColInstance.countDocuments({ _model: 'workflowRun' });
276
- expect(workflowRunCount).toBe(0);
277
- });
278
-
279
- it('devrait déclencher un workflow "DataEdited" lors de la modification de données', async () => {
280
- await initTest();
281
- // 1. Arrange: Insérer une donnée initiale et un déclencheur pour 'DataEdited'
282
- const insertResult = await insertData(targetDataModel.name, { projectName: 'To Edit', status: 'initial' }, {}, mockUser, false);
283
- const projectId = insertResult.insertedIds[0];
284
-
285
- await insertData('workflowTrigger', {
286
- name: 'Trigger on Project Edit',
287
- targetModel: targetDataModel.name,
288
- onEvent: 'DataEdited',
289
- isActive: true,
290
- workflow: testWorkflow._id.toString()
291
- }, {}, mockUser, false);
292
-
293
- // 2. Act: Modifier la donnée
294
- const editResult = await patchData(targetDataModel.name, projectId, { status: 'edited' }, {}, mockUser, true, true);
295
- expect(editResult.success).toBe(true);
296
-
297
- // 2. Act: Modifier la donnée
298
- const editResult2 = await editData(targetDataModel.name, projectId, { projectName: 'test', status: 'edited' }, {}, mockUser, true, true);
299
- expect(editResult2.success).toBe(true);
300
-
301
- const workflows = await testDatasColInstance.countDocuments({ _model: 'workflowRun' });
302
- expect(workflows).toBe(2);
303
- });
304
-
305
- it('devrait déclencher un workflow "DataDeleted" lors de la suppression de données', async () => {
306
- await initTest();
307
- // 1. Arrange: Insérer une donnée et un déclencheur pour 'DataDeleted'
308
- const insertResult = await insertData(targetDataModel.name, { projectName: 'To Delete', status: 'temp' }, {}, mockUser, false);
309
- const projectId = insertResult.insertedIds[0];
310
-
311
- await insertData('workflowTrigger', {
312
- name: 'Trigger on Project Delete',
313
- targetModel: targetDataModel.name,
314
- onEvent: 'DataDeleted',
315
- isActive: true,
316
- workflow: testWorkflow._id.toString()
317
- }, {}, mockUser, false);
318
-
319
- // 2. Act: Supprimer la donnée
320
- const deleteResult = await deleteData(targetDataModel.name, [projectId], mockUser, true, true);
321
- expect(deleteResult.success).toBe(true);
322
-
323
- // 3. Assert: Un `workflowRun` doit être créé
324
- const workflowRun = await testDatasColInstance.findOne({ _model: 'workflowRun' });
325
- expect(workflowRun).not.toBeNull();
326
- // Le `triggerData` est le document *avant* sa suppression
327
- expect(workflowRun.contextData.triggerData.projectName).toBe('To Delete');
328
- });
329
- });
1
+ import { expect, describe, it, beforeEach,afterEach, beforeAll, afterAll, vi } from 'vitest';
2
+ import { Config } from "../src/config";
3
+
4
+ import {insertData, editData, deleteData, patchData} from '../src/index.js';
5
+ import { modelsCollection as getAppModelsCollection, getCollectionForUser } from '../src/modules/mongodb.js';
6
+ import * as workflowModule from '../src/modules/workflow.js';
7
+ import {initEngine} from "../src/setenv.js";
8
+ import {purgeData} from "../src/modules/data/data.history.js";
9
+
10
+ beforeAll(async () =>{
11
+ Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
12
+ await initEngine();
13
+ })
14
+ vi.mock('../src/modules/workflow.js', { spy: true })
15
+ // --- Données Mock pour les tests ---
16
+ const mockUser = {
17
+ username: 'testuserWorkflow',
18
+ _user: 'testuserWorkflow',
19
+ userPlan: 'premium',
20
+ email: 'testWorkflow@example.com'
21
+ };
22
+
23
+ // --- Définitions des modèles ---
24
+
25
+ // Le modèle de données qui déclenchera les workflows
26
+ const targetDataModel = {
27
+ name: 'project',
28
+ _user: mockUser.username,
29
+ description: 'Un modèle pour les projets qui déclencheront des workflows',
30
+ fields: [
31
+ { name: 'projectName', type: 'string', required: true },
32
+ { name: 'status', type: 'string', required: true }, // ex: 'new', 'active', 'archived'
33
+ { name: 'budget', type: 'number' }
34
+ ]
35
+ };
36
+
37
+ // Les modèles qui définissent le système de workflow lui-même.
38
+ // Pour les tests, nous devons insérer leurs définitions dans la collection 'models',
39
+ // puis leurs instances (ex: un trigger spécifique) dans la collection 'datas' de l'utilisateur.
40
+ // Les modèles qui définissent le système de workflow lui-me.
41
+ // Pour les tests, nous devons insérer leurs définitions dans la collection 'models',
42
+ // puis leurs instances (ex: un trigger spécifique) dans la collection 'datas' de l'utilisateur.
43
+ const workflowMetaModels = [
44
+ {
45
+ name: 'workflow',
46
+ _user: mockUser.username,
47
+ description:'',
48
+ fields: [
49
+ { name: 'name', type: 'string' },
50
+ { name: 'startStep', type: 'relation', relation: 'workflowStep' }
51
+ ]
52
+ },
53
+ {
54
+ name: 'workflowStep',
55
+ _user: mockUser.username,
56
+ description:'',
57
+ fields: [
58
+ { name: 'name', type: 'string' },
59
+ { name: 'onSuccessStep', type: 'relation', relation: 'workflowStep' },
60
+ { name: 'onFailureStep', type: 'relation', relation: 'workflowStep' },
61
+ { name: 'isTerminal', type: 'boolean' },
62
+ { name: 'actions', type: 'array', itemsType: 'relation', relation: 'workflowAction' },
63
+ { name: 'conditions', type: 'object' }
64
+ ]
65
+ },
66
+ {
67
+ name: 'workflowTrigger',
68
+ _user: mockUser.username,
69
+ description:'',
70
+ fields: [
71
+ { name: 'name', type: 'string' },
72
+ { name: 'targetModel', type: 'model' },
73
+ { name: 'onEvent', type: 'enum', items: ['DataAdded', 'DataEdited', 'DataDeleted', 'ModelAdded', 'ModelEdited', 'ModelDeleted'] },
74
+ { name: 'isActive', type: 'boolean' },
75
+ { name: 'dataFilter', type: 'object' },
76
+ { name: 'workflow', type: 'relation', relation: 'workflow' },
77
+ { name: 'cronExpression', type: 'cronSchedule' },
78
+ { name: 'lockDurationMinutes', type: 'number' }
79
+ ]
80
+ },
81
+ {
82
+ name: 'workflowRun',
83
+ _user: mockUser.username,
84
+ description:'',
85
+ fields: [
86
+ { name: 'status', type: 'enum', items: ['pending', 'running', 'completed', 'failed', 'cancelled'] },
87
+ { name: 'workflow', type: 'relation', relation: 'workflow' },
88
+ { name: 'contextData', type: 'object' },
89
+ { name: 'currentStep', type: 'relation', relation: 'workflowStep' },
90
+ { name: 'error', type: 'string' },
91
+ { name: 'startedAt', type: 'datetime' },
92
+ { name: 'completedAt', type: 'datetime' },
93
+ { name: 'stepExecutionsCount', type: 'object' }
94
+ ]
95
+ },
96
+ {
97
+ name: 'workflowAction',
98
+ _user: mockUser.username,
99
+ description:'',
100
+ fields: [
101
+ // Common
102
+ { name: 'name', type: 'string' },
103
+ { name: 'type', type: 'enum', items: ['Webhook', 'CreateData', 'UpdateData', 'DeleteData', 'GenerateAIContent', 'SendEmail'] },
104
+ // Webhook
105
+ { name: 'url', type: 'url' },
106
+ { name: 'method', type: 'enum', items: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] },
107
+ { name: 'headers', type: 'code', language: 'json' },
108
+ { name: 'body', type: 'code', language: 'json' },
109
+ // Create/Update/Delete
110
+ { name: 'targetModel', type: 'model' },
111
+ { name: 'dataToCreate', type: 'code', language: 'json' },
112
+ { name: 'targetSelector', type: 'code', language: 'json' },
113
+ { name: 'fieldsToUpdate', type: 'code', language: 'json' },
114
+ { name: 'updateMultiple', type: 'boolean' },
115
+ { name: 'deleteMultiple', type: 'boolean' },
116
+ // GenerateAIContent
117
+ { name: 'aiProvider', type: 'enum', items: ['OpenAI', 'Google', 'Anthropic', 'DeepSeek'] },
118
+ { name: 'aiModel', type: 'string' },
119
+ { name: 'prompt', type: 'richtext' },
120
+ // SendEmail
121
+ { name: 'emailRecipients', type: 'array', itemsType: 'string' },
122
+ { name: 'emailSubject', type: 'string' },
123
+ { name: 'emailContent', type: 'richtext' }
124
+ ]
125
+ }
126
+ ];
127
+
128
+
129
+ let testModelsColInstance;
130
+ let testDatasColInstance;
131
+
132
+ // On "espionne" `c` pour vérifier qu'elle est appelée sans l'exécuter réellement.
133
+ // Cela nous permet de tester uniquement la logique de déclenchement.
134
+ const processWorkflowRunSpy = vi.spyOn(workflowModule, 'processWorkflowRun');
135
+
136
+ beforeEach(async () => {
137
+ testModelsColInstance = getAppModelsCollection;
138
+ testDatasColInstance = await getCollectionForUser(mockUser);
139
+
140
+ // Réinitialiser l'espion
141
+ processWorkflowRunSpy.mockClear();
142
+
143
+ const mods = await testModelsColInstance.find({ $and: [{ _user: mockUser.username}, { $or: [{name: targetDataModel.name}, ...workflowMetaModels.map(m =>({name: m.name}))]}]}).toArray();
144
+ if( mods.length === 0 ) {
145
+ // Insérer les définitions de modèles nécessaires
146
+ await testModelsColInstance.insertMany([
147
+ {...targetDataModel},
148
+ ...workflowMetaModels.map(m => ({...m})) // Copie pour éviter les mutations
149
+ ]);
150
+ }
151
+ // tell vitest we use mocked time
152
+ vi.useFakeTimers({ shouldAdvanceTime: true })
153
+ })
154
+
155
+ afterEach(() => {
156
+ vi.runOnlyPendingTimers();
157
+ // restoring date after each test run
158
+ vi.useRealTimers()
159
+ })
160
+ afterAll(async () => {
161
+ await purgeData(mockUser);
162
+ const coll = await getCollectionForUser(mockUser);
163
+ await coll.drop();
164
+ })
165
+ describe('Intégration des Workflows - triggerWorkflows', () => {
166
+
167
+ let testWorkflow;
168
+ let testStep;
169
+
170
+ // Avant chaque test de ce bloc, on crée un workflow et une étape de base
171
+ const initTest = async () => {
172
+ // Nettoyer les données avant chaque test
173
+ await testDatasColInstance.deleteMany({_user: "testuserWorkflow"});
174
+
175
+ const workflowInsertResult = await insertData('workflow', { name: 'Test Workflow' }, {}, mockUser, false);
176
+ testWorkflow = { _id: workflowInsertResult.insertedIds[0] };
177
+
178
+ const stepInsertResult = await insertData('workflowStep', { name: 'Start Step', isTerminal: true }, {}, mockUser, false);
179
+ testStep = { _id: stepInsertResult.insertedIds[0] };
180
+
181
+ // Lier l'étape au workflow
182
+ await editData('workflow', testWorkflow._id, { startStep: testStep._id.toString() }, {}, mockUser, false);
183
+ };
184
+
185
+ it('devrait créer un workflowRun lors de l\'ajout de données correspondant à un trigger "DataAdded"', async () => {
186
+ await initTest();
187
+ // 1. Arrange: Créer un déclencheur actif pour 'DataAdded' sur le modèle 'project'
188
+ await insertData('workflowTrigger', {
189
+ name: 'Trigger on Project Add',
190
+ targetModel: targetDataModel.name,
191
+ onEvent: 'DataAdded',
192
+ isActive: true,
193
+ workflow: testWorkflow._id.toString()
194
+ }, {}, mockUser, false);
195
+
196
+ // 2. Act: Insérer une donnée qui doit déclencher le workflow
197
+ const projectData = { projectName: 'New Corp Website', status: 'new', budget: 50000 };
198
+ const insertResult = await insertData(targetDataModel.name, projectData, {}, mockUser, true, true);
199
+
200
+ expect(insertResult.success).toBe(true);
201
+
202
+ const workflowRun = await testDatasColInstance.findOne({ _model: 'workflowRun' });
203
+
204
+ expect(workflowRun).not.toBeNull();
205
+ expect(workflowRun.status).toBe('completed');
206
+ expect(workflowRun.workflow.toString()).toBe(testWorkflow._id.toString());
207
+ expect(workflowRun.contextData.triggerData._id.toString()).toBe(insertResult.insertedIds[0].toString());
208
+ expect(workflowRun.contextData.triggerData.projectName).toBe('New Corp Website');
209
+
210
+ // --- Vérification du champ 'history' ---
211
+ expect(workflowRun.history).toBeDefined();
212
+ expect(Array.isArray(workflowRun.history)).toBe(true);
213
+ expect(workflowRun.history.length).toBeGreaterThan(0);
214
+ const firstStepHistory = workflowRun.history[0];
215
+ expect(firstStepHistory.stepId).toBe(testStep._id.toString());
216
+ expect(firstStepHistory.status).toBe('success');
217
+
218
+ });
219
+
220
+ it('ne devrait PAS créer de workflowRun si le trigger est inactif', async () => {
221
+ await initTest();
222
+ // 1. Arrange: Créer un déclencheur INACTIF
223
+ await insertData('workflowTrigger', {
224
+ name: 'Inactive Trigger',
225
+ targetModel: targetDataModel.name,
226
+ onEvent: 'DataAdded',
227
+ isActive: false, // Inactif
228
+ workflow: testWorkflow._id.toString()
229
+ }, {}, mockUser, false);
230
+
231
+ // 2. Act: Insérer des données
232
+ await insertData(targetDataModel.name, { projectName: 'Secret Project', status: 'new' }, {}, mockUser, true);
233
+
234
+ // 3. Assert: Aucun `workflowRun` ne doit être créé
235
+ const workflowRunCount = await testDatasColInstance.countDocuments({ _model: 'workflowRun' });
236
+ expect(workflowRunCount).toBe(0);
237
+ });
238
+
239
+ it('devrait créer un workflowRun si le dataFilter est satisfait', async () => {
240
+ await initTest();
241
+ // 1. Arrange: Créer un déclencheur avec un `dataFilter`
242
+ await insertData('workflowTrigger', {
243
+ name: 'Trigger for Active Projects',
244
+ targetModel: targetDataModel.name,
245
+ onEvent: 'DataAdded',
246
+ isActive: true,
247
+ dataFilter: { $eq: ['$status', 'active'] }, // Filtre pour status = 'active'
248
+ workflow: testWorkflow._id.toString()
249
+ }, {}, mockUser, false);
250
+
251
+ // 2. Act: Insérer des données qui correspondent au filtre
252
+ await insertData(targetDataModel.name, { projectName: 'Go-Live Project', status: 'active' }, {}, mockUser, true, true);
253
+
254
+ // 3. Assert: Un `workflowRun` doit être créé
255
+ const workflowRunCount = await testDatasColInstance.countDocuments({ _model: 'workflowRun' });
256
+ expect(workflowRunCount).toBe(1);
257
+ });
258
+
259
+ it('ne devrait PAS créer de workflowRun si le dataFilter n\'est PAS satisfait', async () => {
260
+ await initTest();
261
+ // 1. Arrange: Créer un déclencheur avec un `dataFilter`
262
+ await insertData('workflowTrigger', {
263
+ name: 'Trigger for Archived Projects',
264
+ targetModel: targetDataModel.name,
265
+ onEvent: 'DataAdded',
266
+ isActive: true,
267
+ dataFilter: { $eq: ['$status', 'archived'] }, // Filtre pour status = 'archived'
268
+ workflow: testWorkflow._id.toString()
269
+ }, {}, mockUser, false);
270
+
271
+ // 2. Act: Insérer des données qui NE correspondent PAS au filtre
272
+ await insertData(targetDataModel.name, { projectName: 'Ongoing Project', status: 'active' }, {}, mockUser, true, true);
273
+
274
+ // 3. Assert: Aucun `workflowRun` ne doit être créé
275
+ const workflowRunCount = await testDatasColInstance.countDocuments({ _model: 'workflowRun' });
276
+ expect(workflowRunCount).toBe(0);
277
+ });
278
+
279
+ it('devrait déclencher un workflow "DataEdited" lors de la modification de données', async () => {
280
+ await initTest();
281
+ // 1. Arrange: Insérer une donnée initiale et un déclencheur pour 'DataEdited'
282
+ const insertResult = await insertData(targetDataModel.name, { projectName: 'To Edit', status: 'initial' }, {}, mockUser, false);
283
+ const projectId = insertResult.insertedIds[0];
284
+
285
+ await insertData('workflowTrigger', {
286
+ name: 'Trigger on Project Edit',
287
+ targetModel: targetDataModel.name,
288
+ onEvent: 'DataEdited',
289
+ isActive: true,
290
+ workflow: testWorkflow._id.toString()
291
+ }, {}, mockUser, false);
292
+
293
+ // 2. Act: Modifier la donnée
294
+ const editResult = await patchData(targetDataModel.name, projectId, { status: 'edited' }, {}, mockUser, true, true);
295
+ expect(editResult.success).toBe(true);
296
+
297
+ // 2. Act: Modifier la donnée
298
+ const editResult2 = await editData(targetDataModel.name, projectId, { projectName: 'test', status: 'edited' }, {}, mockUser, true, true);
299
+ expect(editResult2.success).toBe(true);
300
+
301
+ const workflows = await testDatasColInstance.countDocuments({ _model: 'workflowRun' });
302
+ expect(workflows).toBe(2);
303
+ });
304
+
305
+ it('devrait déclencher un workflow "DataDeleted" lors de la suppression de données', async () => {
306
+ await initTest();
307
+ // 1. Arrange: Insérer une donnée à supprimer et un déclencheur pour 'DataDeleted'
308
+ const insertResult = await insertData(targetDataModel.name, { projectName: 'To Delete', status: 'obsolete' }, {}, mockUser, false);
309
+ const projectId = insertResult.insertedIds[0];
310
+
311
+ await insertData('workflowTrigger', {
312
+ name: 'Trigger on Project Delete',
313
+ targetModel: targetDataModel.name,
314
+ onEvent: 'DataDeleted',
315
+ isActive: true,
316
+ workflow: testWorkflow._id.toString()
317
+ }, {}, mockUser, false);
318
+
319
+ // 2. Act: Supprimer la donnée, en attendant la complétion du workflow
320
+ const deleteResult = await deleteData(targetDataModel.name, [projectId], mockUser, true, true);
321
+ expect(deleteResult.success).toBe(true);
322
+ expect(deleteResult.deletedCount).toBe(1);
323
+
324
+ // 3. Assert: Vérifier que le workflowRun a été créé et est correct
325
+ const workflowRun = await testDatasColInstance.findOne({ _model: 'workflowRun' });
326
+
327
+ expect(workflowRun).not.toBeNull();
328
+ expect(workflowRun.status).toBe('completed');
329
+ expect(workflowRun.contextData.triggerData.projectName).toBe('To Delete');
330
+ expect(workflowRun.contextData.triggerData._id.toString()).toBe(projectId.toString());
331
+ });
332
+ });