data-primals-engine 1.2.2 → 1.2.4

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 (54) hide show
  1. package/CONTRIBUTING.md +91 -0
  2. package/README.md +33 -17
  3. package/client/src/App.jsx +0 -5
  4. package/client/src/ConditionBuilder.scss +34 -1
  5. package/client/src/ConditionBuilder2.jsx +179 -53
  6. package/client/src/ContentView.jsx +0 -3
  7. package/client/src/CronBuilder.jsx +0 -1
  8. package/client/src/CronPartBuilder.jsx +0 -2
  9. package/client/src/DashboardView.jsx +0 -5
  10. package/client/src/DataEditor.jsx +8 -211
  11. package/client/src/DataLayout.jsx +0 -1
  12. package/client/src/DataTable.jsx +1 -3
  13. package/client/src/Field.jsx +0 -5
  14. package/client/src/FlexBuilder.jsx +1 -1
  15. package/client/src/ModelCreatorField.jsx +1 -5
  16. package/client/src/RTE.jsx +1 -6
  17. package/client/src/RTETrans.jsx +0 -2
  18. package/client/src/RelationField.jsx +1 -1
  19. package/client/src/RelationValue.jsx +1 -2
  20. package/client/src/TourSpotlight.jsx +0 -2
  21. package/client/src/constants.js +1 -1
  22. package/client/src/filter.js +87 -0
  23. package/client/src/hooks/data.js +1 -3
  24. package/client/src/hooks/useTutorials.jsx +0 -1
  25. package/package.json +3 -3
  26. package/server.js +2 -2
  27. package/src/constants.js +2 -2
  28. package/src/defaultModels.js +1 -14
  29. package/src/email.js +9 -7
  30. package/src/engine.js +60 -20
  31. package/src/events.js +1 -1
  32. package/src/filter.js +221 -0
  33. package/src/index.js +1 -1
  34. package/src/middlewares/middleware-mongodb.js +0 -1
  35. package/src/modules/assistant.js +1 -3
  36. package/src/modules/bucket.js +3 -4
  37. package/src/modules/{data.js → data/data.js} +42 -59
  38. package/src/modules/data/index.js +1 -0
  39. package/src/modules/file.js +1 -1
  40. package/src/modules/mongodb.js +0 -1
  41. package/src/modules/user.js +1 -1
  42. package/src/modules/workflow.js +299 -133
  43. package/src/packs.js +249 -8
  44. package/test/data.backup.integration.test.js +7 -5
  45. package/test/data.integration.test.js +8 -6
  46. package/test/events.test.js +1 -1
  47. package/test/file.test.js +11 -17
  48. package/test/import_export.integration.test.js +38 -27
  49. package/test/model.integration.test.js +20 -21
  50. package/test/user.test.js +32 -25
  51. package/test/vm.test.js +51 -0
  52. package/test/workflow.integration.test.js +22 -14
  53. package/test/workflow.robustness.test.js +19 -9
  54. package/src/modules/test +0 -147
package/src/packs.js CHANGED
@@ -1,4 +1,4 @@
1
- import {getModels} from "./modules/data.js";
1
+ import {getModels} from "./modules/data/index.js";
2
2
 
3
3
 
4
4
  /*
@@ -39,6 +39,14 @@ export const getAllPacks = async () => {
39
39
  return perms;
40
40
  }
41
41
 
42
+ const envSmtp = [
43
+ { "name": "SMTP_HOST", "value": "smtp.example.com", "description": "SMTP server host for sending emails." },
44
+ { "name": "SMTP_PORT", "value": "587", "description": "SMTP server port." },
45
+ { "name": "SMTP_USER", "value": "user@example.com", "description": "Username for SMTP authentication." },
46
+ { "name": "SMTP_PASS", "value": "your_smtp_password", "description": "Password for SMTP authentication." },
47
+ { "name": "SMTP_FROM", "value": "\"My Store\" <noreply@example.com>", "description": "The 'From' address for outgoing emails." }
48
+ ];
49
+
42
50
  const categories = [ 'News', 'Blog', 'Products', 'Services', 'Store', 'Events', 'Forums', 'Contact', 'Support'];
43
51
  const tags = [ 'info', 'incident', 'maintenance', 'feature', 'hint', 'bugfix', 'question'];
44
52
 
@@ -48,6 +56,242 @@ export const getAllPacks = async () => {
48
56
  {name: 'visitor', perms: ['API_SEARCH_DATA_webpage','API_SEARCH_DATA_content', 'API_SEARCH_DATA_lang', 'API_SEARCH_DATA_currency', 'API_SEARCH_DATA_taxonomy']}];
49
57
 
50
58
  return [
59
+ {
60
+ "name": "Marketing & Campaigning",
61
+ "description": "Launch powerful, personalized, and scalable email campaigns. This pack uses dynamic audiences and a robust workflow to send emails in chunks, ensuring high performance. Depends on the 'Customer Relationship Management (CRM)' pack.",
62
+ "tags": ["marketing", "email", "campaign", "workflow"],
63
+ "models": ["env", "contact", "workflow", "workflowStep", "workflowAction", "workflowRun", "workflowTrigger",
64
+ {
65
+ "name": "campaign",
66
+ "description": "Defines an email marketing campaign.",
67
+ "fields": [
68
+ { "name": "name", "type": "string", "required": true, "asMain": true },
69
+ { "name": "subject", "type": "string", "required": true },
70
+ { "name": "content", "type": "richtext", "required": true },
71
+ { "name": "status", "type": "enum", "items": ["draft", "scheduled", "in_progress", "finished", "cancelled"], "default": "draft" },
72
+ { "name": "audience", "type": "relation", "relation": "audience" },
73
+ { "name": "processedRecipients", "type": "array", "itemsType": "string", "hint": "List of processed contact IDs." }
74
+ ]
75
+ },
76
+ {
77
+ "name": "audience",
78
+ "description": "Defines a dynamic segment of contacts based on a filter.",
79
+ "fields": [
80
+ { "name": "name", "type": "string", "required": true, "asMain": true },
81
+ { "name": "description", "type": "string" },
82
+ { "name": "filter", "type": "code", "language": "json", "required": true, "conditionBuilder": true, "hint": "A MongoDB filter to select contacts. E.g., { \"tags\": \"newsletter\" }" }
83
+ ]
84
+ }
85
+ ],
86
+ "data":{
87
+ "all": {
88
+ "audience": [
89
+ {
90
+ "name": "Example Audience for Campaigning",
91
+ "description": "An example audience targeting specific contacts from the CRM pack.",
92
+ "filter": { "$or": [{ "$eq": ["$email", "alice.martin@innovatech.com"] }, { "$eq": ["$email", "bob.durand@globalexports.com"] }] }
93
+ }
94
+ ],
95
+ "campaign": [
96
+ {
97
+ "name": "Q3 Product Launch",
98
+ "subject": "🚀 Discover Our New Products!",
99
+ "content": "<h1>Hello {recipient.firstName},</h1><p>We are excited to introduce our latest product line. We think you'll love it!</p><p>Best regards,<br>The Team</p>",
100
+ "status": "draft",
101
+ "audience": { "$link": { "name": "Example Audience for Campaigning", "_model": "audience" } }
102
+ }
103
+ ],
104
+ "workflow": [{
105
+ "name": "Campaign Emailing Workflow",
106
+ "description": "A scalable workflow that sends campaign emails in chunks by dynamically querying contacts from an audience.",
107
+ "startStep": { "$link": { "name": "Start Campaign Processing", "_model": "workflowStep" } }
108
+ }],
109
+ "workflowAction": [
110
+ {
111
+ "name": "Set Campaign to 'in_progress'",
112
+ "type": "UpdateData",
113
+ "targetModel": "campaign",
114
+ "targetSelector": { "_id": "{triggerData._id}" },
115
+ "fieldsToUpdate": { "status": "in_progress" }
116
+ },
117
+ {
118
+ "name": "Get Next Recipient Chunk",
119
+ "type": "ExecuteScript",
120
+ "script": `
121
+ const chunkSize = 10; // Process 10 recipients per run
122
+ const campaign = context.triggerData;
123
+ const audience = await db.findOne("audience",{"_id": campaign.audience});
124
+
125
+ if (!audience || !audience.filter) {
126
+ logger.error('Campaign audience or audience filter is not defined.');
127
+ return { chunk: [], message: 'Campaign audience or audience filter is not defined.'}; // Returning an empty chunk will stop the workflow.
128
+ }
129
+
130
+ const processedIds = campaign.processedRecipients || [];
131
+
132
+ const query = {
133
+ '$and': [
134
+ audience.filter,
135
+ { '$not':{ '$in': ["$_id", processedIds] } }
136
+ ]
137
+ };
138
+
139
+ logger.info('Finding next chunk with filter:', JSON.stringify(query));
140
+
141
+ const searchResult = await db.find('contact', query, { limit: chunkSize });
142
+ const chunk = searchResult.data || [];
143
+
144
+ logger.info(\`Found \${chunk.length} recipients for the next chunk.\`);
145
+
146
+ return { chunk }; // This chunk is passed to the next action via context.result
147
+ `
148
+ },
149
+ {
150
+ "name": "Send Email to Chunk",
151
+ "type": "SendEmail",
152
+ "emailRecipients": ["{context.result.chunk}"],
153
+ "emailSubject": "{triggerData.subject}",
154
+ "emailContent": "{triggerData.content}"
155
+ },
156
+ {
157
+ "name": "Update Processed Recipients",
158
+ "type": "ExecuteScript",
159
+ "script": `
160
+ const campaignId = context.triggerData._id;
161
+ const emailResult = context.emailResult;
162
+
163
+ if (!emailResult || !Array.isArray(emailResult.sent) || emailResult.sent.length === 0) {
164
+ logger.info('No recipients were successfully sent an email in this chunk.');
165
+ // Return the original chunk from the first script to allow the condition to check it
166
+ return { processedChunk: context.result.chunk };
167
+ }
168
+
169
+ const processedIds = emailResult.sent.map(recipient => recipient._id.toString());
170
+
171
+ logger.info(\`Updating campaign \${campaignId} with \${processedIds.length} new processed IDs.\`);
172
+
173
+ await db.update(
174
+ 'campaign',
175
+ { _id: campaignId },
176
+ { 'processedRecipients': [...campaign.processedRecipients, ...processedIds] }
177
+ );
178
+
179
+ // Return the original chunk for the condition check step
180
+ return { processedChunk: context.result.chunk };
181
+ `
182
+ },
183
+ {
184
+ "name": "Set Campaign to 'finished'",
185
+ "type": "UpdateData",
186
+ "targetModel": "campaign",
187
+ "targetSelector": { "_id": "{triggerData._id}" },
188
+ "fieldsToUpdate": { "status": "finished" }
189
+ }
190
+ ],
191
+ "workflowStep": [
192
+ {
193
+ "name": "Start Campaign Processing",
194
+ "workflow": { "$link": { "name": "Campaign Emailing Workflow", "_model": "workflow" } },
195
+ "actions": { "$link": { "name": "Set Campaign to 'in_progress'", "_model": "workflowAction" } },
196
+ "onSuccessStep": { "$link": { "name": "Process Recipient Chunk", "_model": "workflowStep" } }
197
+ },
198
+ {
199
+ "name": "Process Recipient Chunk",
200
+ "workflow": { "$link": { "name": "Campaign Emailing Workflow", "_model": "workflow" } },
201
+ "actions":
202
+ { "$link": { "$or": [
203
+ {"$eq": ["$name", "Get Next Recipient Chunk"]},
204
+ {"$eq": ["$name", "Send Email to Chunk"]},
205
+ {"$eq": ["$name", "Update Processed Recipients"]}
206
+ ],"_model": "workflowAction"}},
207
+ "onSuccessStep": { "$link": { "name": "Check if Campaign is Complete", "_model": "workflowStep" } }
208
+ },
209
+ {
210
+ "name": "Check if Campaign is Complete",
211
+ "workflow": { "$link": { "name": "Campaign Emailing Workflow", "_model": "workflow" } },
212
+ "conditions": { "$gt": [{ "$size": {$ifNull:["{context.result.processedChunk}", []]} }, 0] },
213
+ "onSuccessStep": { "$link": { "name": "Process Recipient Chunk", "_model": "workflowStep" } },
214
+ "onFailureStep": { "$link": { "name": "Finish Campaign", "_model": "workflowStep" } }
215
+ },
216
+ {
217
+ "name": "Finish Campaign",
218
+ "workflow": { "$link": { "name": "Campaign Emailing Workflow", "_model": "workflow" } },
219
+ "actions": { "$link": { "name": "Set Campaign to 'finished'", "_model": "workflowAction" } },
220
+ "isTerminal": true
221
+ }
222
+ ],
223
+ "workflowTrigger": [{
224
+ "name": "On Campaign Scheduled",
225
+ "workflow": { "$link": { "name": "Campaign Emailing Workflow", "_model": "workflow" } },
226
+ "type": "manual",
227
+ "onEvent": "DataEdited",
228
+ "targetModel": "campaign",
229
+ "dataFilter": { "$eq": ["$status", "scheduled"] },
230
+ "isActive": true
231
+ }],
232
+ "env": envSmtp
233
+ }
234
+ }
235
+ },
236
+ /*
237
+ {
238
+ "name": "Social Media Publisher",
239
+ "description": "Automate your social media presence. This pack provides a workflow to automatically post new blog articles to Twitter and LinkedIn. Requires the 'Website Starter Pack'.",
240
+ "tags": ["social media", "marketing", "automation", "workflow"],
241
+ "dependencies": ["Website Starter Pack"],
242
+ "models": ["workflow", "workflowStep", "workflowAction", "workflowTrigger", "env"],
243
+ "data": {
244
+ "all": {
245
+ "workflow": [{
246
+ "name": "Publish New Blog Post to Socials",
247
+ "description": "Automatically posts a link to a new blog post on Twitter and LinkedIn.",
248
+ "startStep": {"$link": {"name": "Post on Social Networks", "_model": "workflowStep"}}
249
+ }],
250
+ "workflowAction": [
251
+ {
252
+ "name": "Post to Twitter",
253
+ "type": "PostToSocialMedia",
254
+ "provider": "Twitter",
255
+ "content": "📰 New blog post published: {triggerData.title}! Check it out here: https://your-website.com/blog/{triggerData.slug}"
256
+ },
257
+ {
258
+ "name": "Post to LinkedIn",
259
+ "type": "PostToSocialMedia",
260
+ "provider": "LinkedIn",
261
+ "content": "We've just published a new article: '{triggerData.title}'.\n\n{triggerData.summary}\n\nRead the full post on our blog: https://your-website.com/blog/{triggerData.slug}\n#YourIndustry #BlogPost"
262
+ }
263
+ ],
264
+ "workflowStep": [{
265
+ "name": "Post on Social Networks",
266
+ "workflow": {"$link": {"name": "Publish New Blog Post to Socials", "_model": "workflow"}},
267
+ "actions": {
268
+ "$link": {
269
+ "$or": [
270
+ {"$eq": ["$name", "Post to Twitter"]},
271
+ {"$eq": ["$name", "Post to LinkedIn"]}
272
+ ],
273
+ "_model": "workflowAction"
274
+ }
275
+ },
276
+ "isTerminal": true
277
+ }],
278
+ "workflowTrigger": [{
279
+ "name": "On New Blog Post Added",
280
+ "workflow": {"$link": {"name": "Publish New Blog Post to Socials", "_model": "workflow"}},
281
+ "type": "manual",
282
+ "onEvent": "DataAdded",
283
+ "targetModel": "content",
284
+ "dataFilter": {"category": {"$find": {"name": "Blog"}}},
285
+ "isActive": true
286
+ }],
287
+ "env": [
288
+ {"name": "TWITTER_API_KEY", "value": "your_key_here"},
289
+ {"name": "TWITTER_API_SECRET", "value": "your_secret_here"},
290
+ {"name": "LINKEDIN_ACCESS_TOKEN", "value": "your_token_here"}
291
+ ]
292
+ }
293
+ }
294
+ },*/
51
295
  {
52
296
  "name": "E-commerce Starter Kit",
53
297
  "description": "Launch your online store in just a few clicks. This pack includes templates for products, orders, and customers, as well as sample data, KPIs, alerts and an order fulfillment workflow (with sending email).",
@@ -55,13 +299,7 @@ export const getAllPacks = async () => {
55
299
  "models": ["env", "taxonomy", "product", "productVariant", "brand", "currency", "order", "shipment", "review", "cart", "cartItem", "discount", "workflow", "workflowStep", "workflowAction","workflowRun", "workflowTrigger", "translation", "lang", "kpi", "dashboard", "alert", "return"],
56
300
  "data": {
57
301
  "all": {
58
- "env": [
59
- { "name": "SMTP_HOST", "value": "smtp.example.com", "description": "SMTP server host for sending emails." },
60
- { "name": "SMTP_PORT", "value": "587", "description": "SMTP server port." },
61
- { "name": "SMTP_USER", "value": "user@example.com", "description": "Username for SMTP authentication." },
62
- { "name": "SMTP_PASS", "value": "your_smtp_password", "description": "Password for SMTP authentication." },
63
- { "name": "SMTP_FROM", "value": "\"My Store\" <noreply@example.com>", "description": "The 'From' address for outgoing emails." }
64
- ],
302
+ "env":envSmtp,
65
303
  "taxonomy": [
66
304
  { "name": "E-commerce", "type": "category" },
67
305
  { "name": "Clothes", "type": "category", "parent": { "$find": { "name": "E-commerce" } } },
@@ -1122,6 +1360,9 @@ export const getAllPacks = async () => {
1122
1360
  },{
1123
1361
  name: "GOOGLE_API_KEY",
1124
1362
  value: "demo"
1363
+ },{
1364
+ name: "DEEPSEEK_API_KEY",
1365
+ value: "demo"
1125
1366
  }]
1126
1367
  }
1127
1368
  }
@@ -10,19 +10,19 @@ import { vi } from 'vitest'
10
10
  import {
11
11
  createModel,
12
12
  insertData
13
- } from 'data-primals-engine/modules/data';
13
+ } from '../src/index.js';
14
14
 
15
15
  import {
16
16
  modelsCollection as getAppModelsCollection,
17
- getCollectionForUser as getAppUserCollection
18
- } from 'data-primals-engine/modules/mongodb';
17
+ getCollectionForUser as getAppUserCollection, getCollectionForUser
18
+ } from '../src/modules/mongodb.js';
19
19
  import process from "node:process";
20
20
 
21
- import { dumpUserData, loadFromDump } from 'data-primals-engine/modules/data';
21
+ import { dumpUserData, loadFromDump } from '../src/index.js';
22
22
  import fs from "node:fs";
23
23
  import {initEngine} from "../src/setenv.js";
24
24
 
25
- vi.mock('data-primals-engine/engine', async(importOriginal) => {
25
+ vi.mock('../src/engine', async(importOriginal) => {
26
26
  const mod = await importOriginal() // type is inferred
27
27
  return {
28
28
  ...mod
@@ -88,6 +88,8 @@ afterAll(async () => {
88
88
  });
89
89
  fs.rmdirSync(backupDir); // Remove the directory itself
90
90
  }
91
+ const coll = await getCollectionForUser(mockUser);
92
+ await coll.drop()
91
93
  });
92
94
 
93
95
  beforeAll(async () =>{
@@ -1,20 +1,19 @@
1
1
  // __tests__/data.integration.test.js
2
2
  import { ObjectId } from 'mongodb';
3
- import {expect, describe, it, beforeEach, beforeAll, afterAll} from 'vitest';
3
+ import {vi, expect, describe, it, beforeEach, beforeAll, afterAll} from 'vitest';
4
4
  import { Config } from '../src/config.js';
5
-
6
5
  import {
7
6
  insertData,
8
7
  editData,
9
8
  deleteData,
10
- searchData, installPack, deleteModels, createModel
11
- } from 'data-primals-engine/modules/data';
9
+ searchData, installPack, deleteModels, createModel, patchData
10
+ } from '../src/index.js';
12
11
 
13
12
  import {
14
13
  modelsCollection as getAppModelsCollection,
15
14
  getCollection,
16
- getCollectionForUser as getAppUserCollection
17
- } from 'data-primals-engine/modules/mongodb';
15
+ getCollectionForUser as getAppUserCollection, getCollectionForUser
16
+ } from '../src/modules/mongodb.js';
18
17
  import {getRandom} from "../src/core.js";
19
18
  import {generateUniqueName, initEngine} from "../src/setenv.js";
20
19
 
@@ -828,6 +827,8 @@ describe('Intégration des fonctions CRUD de données avec validation complète'
828
827
  await deleteModels({ name: orderModel.name, _user: user.username });
829
828
  await deleteData(productModel.name, {}, user);
830
829
  await deleteData(orderModel.name, {}, user);
830
+ const coll = await getCollectionForUser(user);
831
+ await coll.drop();
831
832
  });
832
833
 
833
834
  it('should ALLOW inserting data with a valid relation', async () => {
@@ -871,4 +872,5 @@ describe('Intégration des fonctions CRUD de données avec validation complète'
871
872
  await deleteData(orderModel.name, initialOrder.insertedIds, user);
872
873
  });
873
874
  });
875
+
874
876
  });
@@ -1,7 +1,7 @@
1
1
  // events.test.js
2
2
  import { Event } from '../src/events.js';
3
3
  import {vitest} from "vitest";
4
- import {expect, describe, it, beforeEach, beforeAll, afterAll} from 'vitest';
4
+ import {expect, describe, it, beforeEach} from 'vitest';
5
5
 
6
6
  describe('Event System', () => {
7
7
  beforeEach(() => {
package/test/file.test.js CHANGED
@@ -1,13 +1,11 @@
1
- // __tests__/file.integration.test.js
2
- import { expect, describe, it, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest';
1
+ import { expect, describe, it, beforeEach, beforeAll, afterAll, vi } from 'vitest';
3
2
  import { Config } from '../src/config.js';
4
3
  import fs from 'node:fs';
5
4
  import path from 'node:path';
6
5
  import { fileURLToPath } from 'node:url';
7
- import { ObjectId } from 'mongodb';
8
6
  import { initEngine, generateUniqueName } from "../src/setenv.js";
9
7
  import { addFile, removeFile, getFile, onInit } from "../src/modules/file.js";
10
- import { getCollection } from "../src/modules/mongodb.js";
8
+ import {getCollection, getUserCollectionName} from "../src/modules/mongodb.js";
11
9
  import { Logger } from "../src/gameObject.js";
12
10
  import {maxPrivateFileSize} from "../src/constants.js";
13
11
 
@@ -46,19 +44,17 @@ beforeAll(async () => {
46
44
 
47
45
  filesCollection = await getCollection("files");
48
46
  });
49
- describe('File Module Integration Tests', () => {
50
- let testUser;
47
+ let testUser = {
48
+ username: generateUniqueName('testuser'),
49
+ _user: generateUniqueName('testuser'),
50
+ userPlan: 'premium',
51
+ permissions: ['API_UPLOAD_FILE']
52
+ };
51
53
 
54
+ describe('File Module Integration Tests', () => {
52
55
 
53
56
  beforeEach(async () => {
54
57
  // Créer un utilisateur de test
55
- testUser = {
56
- username: generateUniqueName('testuser'),
57
- _user: generateUniqueName('testuser'),
58
- userPlan: 'premium',
59
- permissions: ['API_UPLOAD_FILE']
60
- };
61
-
62
58
  // Nettoyer les collections avant chaque test
63
59
  await filesCollection.deleteMany({});
64
60
 
@@ -109,8 +105,7 @@ describe('File Module Integration Tests', () => {
109
105
  await expect(addFile(mockFile, testUser)).rejects.toThrow(/La taille du fichier dépasse la limite autorisée/);
110
106
  });
111
107
 
112
- it('should throw error when storage limit exceeded', async ({skip}) => {
113
- skip();
108
+ it.skip('should throw error when storage limit exceeded', async ({skip}) => {
114
109
  // Mock la fonction de calcul d'usage pour simuler un dépassement
115
110
  vi.spyOn(engine.userProvider, 'getUserStorageLimit').mockResolvedValue(85 * 1024 * 1024); // 5MB
116
111
 
@@ -119,8 +114,7 @@ describe('File Module Integration Tests', () => {
119
114
  await expect(addFile(mockFile, testUser)).rejects.toThrow("api.data.storageLimitExceeded");
120
115
  });
121
116
 
122
- it('should throw error when server capacity is insufficient', async ({skip}) => {
123
- skip();
117
+ it.skip('should throw error when server capacity is insufficient', async ({skip}) => {
124
118
  // Mock la vérification de capacité serveur
125
119
  vi.spyOn(engine.userProvider, 'checkServerCapacity').mockResolvedValue({ isSufficient: false });
126
120
 
@@ -1,20 +1,19 @@
1
- import {expect, describe, it, beforeEach, beforeAll, afterAll, vi} from 'vitest';
1
+ import {expect, describe, it, beforeEach, beforeAll, afterAll, vi, afterEach} from 'vitest';
2
2
 
3
3
  // --- Importations des modules de votre application ---
4
4
  import {
5
- createModel,
6
5
  insertData,
7
6
  exportData,
8
7
  importData
9
- } from 'data-primals-engine/modules/data';
8
+ } from '../src/index.js';
10
9
 
11
10
  import {
12
11
  modelsCollection as getAppModelsCollection,
13
- getCollectionForUser as getAppUserCollection
14
- } from 'data-primals-engine/modules/mongodb';
12
+ getCollectionForUser as getAppUserCollection, getCollectionForUser
13
+ } from '../src/modules/mongodb.js';
15
14
  import {sleep} from "data-primals-engine/core";
16
15
  import fs from "node:fs";
17
- import {getUniquePort, initEngine} from "../src/setenv.js";
16
+ import {initEngine} from "../src/setenv.js";
18
17
  import {Config} from "../src/index.js";
19
18
 
20
19
  // --- Données Mock ---
@@ -38,13 +37,9 @@ const impexTestModel = {
38
37
  };
39
38
 
40
39
  // --- Setup de l'environnement de test ---
41
- let mongod;
42
- let testDbUri;
43
- const testDbName = 'testIntegrationDbHO_Impex';
44
40
  let testModelsColInstance;
45
41
  let testDatasColInstance;
46
- let engineInstance;
47
- const port = getUniquePort(); // Port unique pour cette suite de tests
42
+
48
43
  function blobToFile(theBlob, fileName){
49
44
  //A Blob() is almost a File() - it's just missing the two properties below which we will add
50
45
  theBlob.lastModifiedDate = new Date();
@@ -56,6 +51,21 @@ beforeAll(async () =>{
56
51
  Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
57
52
  await initEngine();
58
53
  })
54
+
55
+ beforeEach(async() =>{
56
+ // tell vitest we use mocked time
57
+ vi.useFakeTimers({ shouldAdvanceTime: true })
58
+ })
59
+
60
+ afterEach(() => {
61
+ vi.runOnlyPendingTimers();
62
+ // restoring date after each test run
63
+ vi.useRealTimers()
64
+ })
65
+ afterAll(async () => {
66
+ const coll = await getCollectionForUser(mockUser);
67
+ await coll.drop();
68
+ })
59
69
  // --- Début des tests ---
60
70
  describe('Intégration des fonctions d\'Import/Export', () => {
61
71
 
@@ -80,6 +90,7 @@ describe('Intégration des fonctions d\'Import/Export', () => {
80
90
 
81
91
  describe('Export de données', () => {
82
92
  it('devrait exporter les données en format JSON', async () => {
93
+
83
94
  const res= await exportData({
84
95
  models: [impexTestModel.name],
85
96
  depth: 1
@@ -103,6 +114,7 @@ describe('Intégration des fonctions d\'Import/Export', () => {
103
114
 
104
115
  describe('Import de données', () => {
105
116
  it('devrait importer des données depuis une chaîne JSON', async () => {
117
+
106
118
  const jsonDataToImport = [
107
119
  { name: 'Produit D', sku: 'SKU-D', price: 1.00, inStock: true },
108
120
  { name: 'Produit E', sku: 'SKU-E', price: 2.00 } // inStock utilisera la valeur par défaut
@@ -114,13 +126,13 @@ describe('Intégration des fonctions d\'Import/Export', () => {
114
126
  fs.writeFileSync('test.json', jsonString);
115
127
  blob.path = 'test.json';
116
128
  blob.originalFilename = 'test.json';
117
- const result = await importData({model: impexTestModel.name}, {file: blobToFile(blob,"test.json")}, mockUser);
129
+ const result = await importData({model: impexTestModel.name}, {file: blobToFile(blob, "test.json")}, mockUser);
118
130
 
119
131
  // Vérifications du résultat de l'opération
120
132
  expect(result.success).toBe(true);
121
- expect(result.jobId).not.toBeNull();
133
+ expect(result.job.jobId).not.toBeNull();
122
134
 
123
- await sleep(2000);
135
+ await sleep(5000);
124
136
 
125
137
  // Vérification directe en base de données
126
138
  const importedDocs = await testDatasColInstance.find({
@@ -136,7 +148,7 @@ describe('Intégration des fonctions d\'Import/Export', () => {
136
148
  expect(docD.inStock).toBe(true); // Vérification de la valeur par défaut
137
149
  expect(docE.price).toBe(2.00);
138
150
 
139
- }, 5000);
151
+ }, 20000);
140
152
 
141
153
  it('devrait importer des données depuis une chaîne CSV et convertir les types', async () => {
142
154
  const csvStringToImport = `name,sku,price,inStock\nProduit F,SKU-F,3.55,true\nProduit G,SKU-G,4.99,false`;
@@ -148,14 +160,13 @@ describe('Intégration des fonctions d\'Import/Export', () => {
148
160
  blob.originalFilename = 'test.csv';
149
161
 
150
162
  // Exécution de la fonction d'import
151
- const result = await importData({model:impexTestModel.name}, {file: blobToFile(blob,"test.csv")}, mockUser);
163
+ const result = await importData({model:impexTestModel.name}, {file: blobToFile(blob, "test.csv")}, mockUser);
152
164
 
153
- console.log(result)
154
165
  // Vérifications du résultat
155
166
  expect(result.success).toBe(true);
167
+ expect(result.job.jobId).not.toBeNull();
156
168
 
157
- await sleep(2000);
158
-
169
+ await sleep(5000);
159
170
  // Vérification en base de données
160
171
  const importedDocs = await testDatasColInstance.find({
161
172
  _model: impexTestModel.name,
@@ -185,16 +196,16 @@ Valide K,SKU-A,40,true`;
185
196
  blob.path = 'test.csv';
186
197
  blob.originalFilename = 'test.csv';
187
198
 
188
- const result = await importData(impexTestModel.name, {file: blobToFile(blob,"test.csv")}, 'csv', mockUser);
199
+ const result = await importData({ model: impexTestModel.name }, {file: blobToFile(blob,"test.csv")}, mockUser);
189
200
 
190
- console.log(result);
191
- // Vérifications du résultat
192
- expect(result.success).toBe(true); // L'opération globale a des erreurs
193
- expect(result.job.status).toBe('failed'); // L'opération globale a des erreurs
201
+ // L'initiation du job doit réussir
202
+ expect(result.success).toBe(true);
203
+ expect(result.job.jobId).not.toBeNull();
194
204
 
205
+ await sleep(5000);
195
206
  // Vérifier que seule les données valides sont en BDD
196
- const count = await testDatasColInstance.countDocuments({ _model: impexTestModel.name });
197
- expect(count).toBe(3);
198
- });
207
+ const count = await testDatasColInstance.countDocuments({ _model: impexTestModel.name, sku: 'SKU-H' });
208
+ expect(count).toBe(1); // Seule la ligne valide 'SKU-H' doit être insérée.
209
+ }, 20000);
199
210
  });
200
211
  });