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.
- package/README.md +160 -160
- package/client/package-lock.json +1080 -212
- package/client/package.json +12 -6
- package/client/src/AssistantChat.jsx +1 -3
- package/client/src/DataLayout.jsx +19 -20
- package/client/src/DataTable.jsx +2 -2
- package/client/src/DocumentationPageLayout.scss +1 -1
- package/client/src/ViewSwitcher.jsx +1 -1
- package/client/vite.config.js +31 -30
- package/package.json +39 -23
- package/src/ai.jobs.js +135 -0
- package/src/constants.js +561 -545
- package/src/core.js +487 -477
- package/src/data.js +2 -0
- package/src/email.js +0 -2
- package/src/engine.js +50 -42
- package/src/filter.js +348 -343
- 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/bucket.js +4 -0
- package/src/modules/data/data.cluster.js +191 -0
- package/src/modules/data/data.core.js +11 -8
- package/src/modules/data/data.js +13 -4
- package/src/modules/data/data.operations.js +186 -106
- package/src/modules/data/data.relations.js +1 -0
- package/src/modules/data/data.replication.js +83 -0
- package/src/modules/data/data.routes.js +2183 -1879
- package/src/modules/mongodb.js +76 -73
- package/src/modules/user.js +7 -1
- package/src/modules/worker-script-runner.js +97 -0
- package/src/modules/workflow.js +1953 -1815
- package/src/packs.js +5701 -5697
- package/src/providers.js +298 -297
- package/test/assistant.test.js +207 -206
- package/test/data.integration.test.js +1425 -1416
- package/test/import_export.integration.test.js +210 -210
- package/test/workflow.actions.integration.test.js +487 -475
- 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: '
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
expect(workflowRun.
|
|
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
|
+
});
|