data-primals-engine 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/packs.js CHANGED
@@ -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" } } },
@@ -14,13 +14,14 @@ import {
14
14
 
15
15
  import {
16
16
  modelsCollection as getAppModelsCollection,
17
- getCollectionForUser as getAppUserCollection
17
+ getCollectionForUser as getAppUserCollection, getCollectionForUser
18
18
  } from 'data-primals-engine/modules/mongodb';
19
19
  import process from "node:process";
20
20
 
21
21
  import { dumpUserData, loadFromDump } from 'data-primals-engine/modules/data';
22
22
  import fs from "node:fs";
23
23
  import {initEngine} from "../src/setenv.js";
24
+ import {getUserCollectionName} from "../src/modules/mongodb.js";
24
25
 
25
26
  vi.mock('data-primals-engine/engine', async(importOriginal) => {
26
27
  const mod = await importOriginal() // type is inferred
@@ -88,6 +89,8 @@ afterAll(async () => {
88
89
  });
89
90
  fs.rmdirSync(backupDir); // Remove the directory itself
90
91
  }
92
+ const coll = await getCollectionForUser(mockUser);
93
+ await coll.drop()
91
94
  });
92
95
 
93
96
  beforeAll(async () =>{
@@ -1,22 +1,25 @@
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
9
+ searchData, installPack, deleteModels, createModel, patchData
11
10
  } from 'data-primals-engine/modules/data';
12
11
 
13
12
  import {
14
13
  modelsCollection as getAppModelsCollection,
15
14
  getCollection,
16
- getCollectionForUser as getAppUserCollection
15
+ getCollectionForUser as getAppUserCollection, getCollectionForUser
17
16
  } from 'data-primals-engine/modules/mongodb';
18
17
  import {getRandom} from "../src/core.js";
19
18
  import {generateUniqueName, initEngine} from "../src/setenv.js";
19
+ import {processWorkflowRun} from "data-primals-engine/modules/workflow";
20
+ import {sendEmail} from "../src/email.js";
21
+ import {MongoDatabase} from "../src/engine.js";
22
+ import {getUserCollectionName} from "../src/modules/mongodb.js";
20
23
 
21
24
  let testModelsColInstance;
22
25
  let testDatasColInstance;
@@ -828,6 +831,8 @@ describe('Intégration des fonctions CRUD de données avec validation complète'
828
831
  await deleteModels({ name: orderModel.name, _user: user.username });
829
832
  await deleteData(productModel.name, {}, user);
830
833
  await deleteData(orderModel.name, {}, user);
834
+ const coll = await getCollectionForUser(user);
835
+ await coll.drop();
831
836
  });
832
837
 
833
838
  it('should ALLOW inserting data with a valid relation', async () => {
@@ -871,4 +876,5 @@ describe('Intégration des fonctions CRUD de données avec validation complète'
871
876
  await deleteData(orderModel.name, initialOrder.insertedIds, user);
872
877
  });
873
878
  });
879
+
874
880
  });
package/test/file.test.js CHANGED
@@ -7,9 +7,10 @@ import { fileURLToPath } from 'node:url';
7
7
  import { ObjectId } from 'mongodb';
8
8
  import { initEngine, generateUniqueName } from "../src/setenv.js";
9
9
  import { addFile, removeFile, getFile, onInit } from "../src/modules/file.js";
10
- import { getCollection } from "../src/modules/mongodb.js";
10
+ import {getCollection, getUserCollectionName} from "../src/modules/mongodb.js";
11
11
  import { Logger } from "../src/gameObject.js";
12
12
  import {maxPrivateFileSize} from "../src/constants.js";
13
+ import {getCollectionForUser} from "data-primals-engine/modules/mongodb";
13
14
 
14
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
16
 
@@ -46,19 +47,17 @@ beforeAll(async () => {
46
47
 
47
48
  filesCollection = await getCollection("files");
48
49
  });
49
- describe('File Module Integration Tests', () => {
50
- let testUser;
50
+ let testUser = {
51
+ username: generateUniqueName('testuser'),
52
+ _user: generateUniqueName('testuser'),
53
+ userPlan: 'premium',
54
+ permissions: ['API_UPLOAD_FILE']
55
+ };
51
56
 
57
+ describe('File Module Integration Tests', () => {
52
58
 
53
59
  beforeEach(async () => {
54
60
  // 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
61
  // Nettoyer les collections avant chaque test
63
62
  await filesCollection.deleteMany({});
64
63
 
@@ -109,8 +108,7 @@ describe('File Module Integration Tests', () => {
109
108
  await expect(addFile(mockFile, testUser)).rejects.toThrow(/La taille du fichier dépasse la limite autorisée/);
110
109
  });
111
110
 
112
- it('should throw error when storage limit exceeded', async ({skip}) => {
113
- skip();
111
+ it.skip('should throw error when storage limit exceeded', async ({skip}) => {
114
112
  // Mock la fonction de calcul d'usage pour simuler un dépassement
115
113
  vi.spyOn(engine.userProvider, 'getUserStorageLimit').mockResolvedValue(85 * 1024 * 1024); // 5MB
116
114
 
@@ -119,8 +117,7 @@ describe('File Module Integration Tests', () => {
119
117
  await expect(addFile(mockFile, testUser)).rejects.toThrow("api.data.storageLimitExceeded");
120
118
  });
121
119
 
122
- it('should throw error when server capacity is insufficient', async ({skip}) => {
123
- skip();
120
+ it.skip('should throw error when server capacity is insufficient', async ({skip}) => {
124
121
  // Mock la vérification de capacité serveur
125
122
  vi.spyOn(engine.userProvider, 'checkServerCapacity').mockResolvedValue({ isSufficient: false });
126
123
 
@@ -2,20 +2,20 @@ import {expect, describe, it, beforeEach, beforeAll, afterAll, vi} 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
12
+ getCollectionForUser as getAppUserCollection, getCollectionForUser
14
13
  } from 'data-primals-engine/modules/mongodb';
15
14
  import {sleep} from "data-primals-engine/core";
16
15
  import fs from "node:fs";
17
16
  import {getUniquePort, initEngine} from "../src/setenv.js";
18
17
  import {Config} from "../src/index.js";
18
+ import {ObjectId} from "mongodb";
19
19
 
20
20
  // --- Données Mock ---
21
21
  const mockUser = {
@@ -56,6 +56,10 @@ beforeAll(async () =>{
56
56
  Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
57
57
  await initEngine();
58
58
  })
59
+ afterAll(async () => {
60
+ const coll = await getCollectionForUser(mockUser);
61
+ await coll.drop();
62
+ })
59
63
  // --- Début des tests ---
60
64
  describe('Intégration des fonctions d\'Import/Export', () => {
61
65
 
@@ -114,13 +118,13 @@ describe('Intégration des fonctions d\'Import/Export', () => {
114
118
  fs.writeFileSync('test.json', jsonString);
115
119
  blob.path = 'test.json';
116
120
  blob.originalFilename = 'test.json';
117
- const result = await importData({model: impexTestModel.name}, {file: blobToFile(blob,"test.json")}, mockUser);
121
+ const result = await importData({model: impexTestModel.name}, {file: blobToFile(blob, "test.json")}, mockUser);
118
122
 
119
123
  // Vérifications du résultat de l'opération
120
124
  expect(result.success).toBe(true);
121
- expect(result.jobId).not.toBeNull();
125
+ expect(result.job.jobId).not.toBeNull();
122
126
 
123
- await sleep(2000);
127
+ await sleep(8000)
124
128
 
125
129
  // Vérification directe en base de données
126
130
  const importedDocs = await testDatasColInstance.find({
@@ -136,7 +140,7 @@ describe('Intégration des fonctions d\'Import/Export', () => {
136
140
  expect(docD.inStock).toBe(true); // Vérification de la valeur par défaut
137
141
  expect(docE.price).toBe(2.00);
138
142
 
139
- }, 5000);
143
+ }, 20000);
140
144
 
141
145
  it('devrait importer des données depuis une chaîne CSV et convertir les types', async () => {
142
146
  const csvStringToImport = `name,sku,price,inStock\nProduit F,SKU-F,3.55,true\nProduit G,SKU-G,4.99,false`;
@@ -148,14 +152,13 @@ describe('Intégration des fonctions d\'Import/Export', () => {
148
152
  blob.originalFilename = 'test.csv';
149
153
 
150
154
  // Exécution de la fonction d'import
151
- const result = await importData({model:impexTestModel.name}, {file: blobToFile(blob,"test.csv")}, mockUser);
155
+ const result = await importData({model:impexTestModel.name}, {file: blobToFile(blob, "test.csv")}, mockUser);
152
156
 
153
- console.log(result)
154
157
  // Vérifications du résultat
155
158
  expect(result.success).toBe(true);
159
+ expect(result.job.jobId).not.toBeNull();
156
160
 
157
- await sleep(2000);
158
-
161
+ await sleep(8000);
159
162
  // Vérification en base de données
160
163
  const importedDocs = await testDatasColInstance.find({
161
164
  _model: impexTestModel.name,
@@ -185,16 +188,17 @@ Valide K,SKU-A,40,true`;
185
188
  blob.path = 'test.csv';
186
189
  blob.originalFilename = 'test.csv';
187
190
 
188
- const result = await importData(impexTestModel.name, {file: blobToFile(blob,"test.csv")}, 'csv', mockUser);
191
+ const result = await importData({ model: impexTestModel.name }, {file: blobToFile(blob,"test.csv")}, mockUser);
189
192
 
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
193
+ // L'initiation du job doit réussir
194
+ expect(result.success).toBe(true);
195
+ expect(result.job.jobId).not.toBeNull();
196
+
197
+ await sleep(8000);
194
198
 
195
199
  // 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
- });
200
+ const count = await testDatasColInstance.countDocuments({ _model: impexTestModel.name, sku: 'SKU-H' });
201
+ expect(count).toBe(1); // Seule la ligne valide 'SKU-H' doit être insérée.
202
+ }, 20000);
199
203
  });
200
204
  });
@@ -5,30 +5,31 @@ import { Config } from '../src/config.js';
5
5
 
6
6
  import {
7
7
  modelsCollection as getAppModelsCollection,
8
- datasCollection // Accès direct pour vérifications
8
+ datasCollection, getCollection // Accès direct pour vérifications
9
9
  } from 'data-primals-engine/modules/mongodb';
10
10
  import {generateUniqueName, getUniquePort, initEngine} from "../src/setenv.js";
11
11
  import {editModel} from "../src/modules/data.js";
12
- import {getCollectionForUser} from "../src/modules/mongodb.js";
12
+ import {getCollectionForUser, getUserCollectionName} from "../src/modules/mongodb.js";
13
13
 
14
14
  let testModelsColInstance;
15
- let testDatasColInstance;
16
-
17
15
  let testModelId;
18
-
16
+ let lastUser;
19
17
  // Cette fonction va remplacer la logique de votre beforeEach pour la création de contexte
20
18
  async function setupTestContext() {
21
19
 
22
20
  const currentTestModelName = generateUniqueName('relatedModel');
23
21
  const currentRelatedModelName = generateUniqueName('comprehensiveModel');
24
22
 
23
+
25
24
  // Créer un utilisateur unique pour ce test
26
25
  const currentTestUser = {
27
- username: generateUniqueName('testuserDataIntegration'),
28
- userPlan: 'free',
26
+ username: generateUniqueName('testuserModelIntegration'),
27
+ userPlan: 'premium',
29
28
  email: generateUniqueName('test') + '@example.com'
30
29
  };
31
30
 
31
+ testModelsColInstance = getCollection("models");
32
+ const testDatasColInstance = await getCollectionForUser(currentTestUser);
32
33
 
33
34
  const relatedModelDefinition = {
34
35
  name: currentRelatedModelName,
@@ -105,43 +106,42 @@ async function setupTestContext() {
105
106
  await testDatasColInstance.deleteMany({ _user: currentTestUser.username });
106
107
  await testDatasColInstance.deleteMany({ _model: { $in: [comprehensiveTestModelDefinition.name, 'renamedTestModel'] } });
107
108
  // Retourner toutes les variables nécessaires pour un test
109
+
108
110
  return {
109
111
  currentTestUser,
112
+ coll:testDatasColInstance,
110
113
  comprehensiveTestModelDefinition,
111
114
  relatedModelDefinition
112
115
  };
113
116
  }
114
117
 
118
+
115
119
  describe('CRUD on model definitions and integrity tests', () => {
116
120
 
117
121
  beforeAll(async () =>{
118
122
  Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
119
123
  await initEngine();
120
-
121
- // Initialize collection instances after the engine is ready
122
- testModelsColInstance = getAppModelsCollection;
123
- testDatasColInstance = datasCollection;
124
124
  })
125
+
125
126
  describe('editModel unit tests', () => {
126
127
 
127
- it('should create and drop index when field.index is toggled (premium user)', async () => {
128
+ it.skip('should create and drop index when field.index is toggled (premium user)', async () => {
128
129
  // --- SETUP ---
129
- const { currentTestUser, comprehensiveTestModelDefinition } = await setupTestContext();
130
- const dataCollection = await getCollectionForUser(currentTestUser);
130
+ const { coll, currentTestUser, comprehensiveTestModelDefinition } = await setupTestContext();
131
131
  const fieldToIndex = 'stringUnique'; // Utiliser un champ qui existe vraiment dans le modèle
132
132
 
133
133
  // --- FIX: Ensure the collection exists before any operation ---
134
134
  // By inserting and deleting a dummy document, we force MongoDB to create the
135
135
  // collection and its default indexes. This prevents the "ns does not exist"
136
136
  // error in asynchronous listeners (like workflow triggers).
137
- const dummyDoc = await dataCollection.insertOne({ _model: 'dummy', _user: currentTestUser.username });
138
- await dataCollection.deleteOne({ _id: dummyDoc.insertedId });
137
+ const dummyDoc = await coll.insertOne({ _model: 'dummy', _user: currentTestUser.username });
138
+ await coll.deleteOne({ _id: dummyDoc.insertedId });
139
139
 
140
140
 
141
141
  // --- VERIFICATION INITIALE ---
142
142
  // S'assurer qu'aucun index n'existe au départ.
143
143
  // Cet appel ne plantera plus car la collection est maintenant créée.
144
- const initialIndexes = await dataCollection.indexes();
144
+ const initialIndexes = await coll.indexes();
145
145
  expect(initialIndexes.some(i => i.key[fieldToIndex] === 1)).toBe(false);
146
146
 
147
147
  // --- ACTION 1 : AJOUTER UN INDEX ---
@@ -155,7 +155,7 @@ describe('CRUD on model definitions and integrity tests', () => {
155
155
 
156
156
  // --- VERIFICATION 1 ---
157
157
  // Maintenant, la collection et l'index doivent exister.
158
- const indexesAfterCreation = await dataCollection.indexes();
158
+ const indexesAfterCreation = await coll.indexes();
159
159
  const newIndex = indexesAfterCreation.find(i => i.key[fieldToIndex] === 1);
160
160
 
161
161
  expect(newIndex).toBeDefined();
@@ -175,8 +175,9 @@ describe('CRUD on model definitions and integrity tests', () => {
175
175
  await editModel(currentTestUser, testModelId, modelWithoutIndex);
176
176
 
177
177
  // --- VERIFICATION 2 ---
178
- const indexesAfterDeletion = await dataCollection.indexes();
178
+ const indexesAfterDeletion = await coll.indexes();
179
179
  expect(indexesAfterDeletion.some(i => i.key[fieldToIndex] === 1)).toBe(false);
180
+
180
181
  }, 20000);
181
182
 
182
183
  it('should not save extra, non-defined fields in the model definition', async () => {