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/client/src/DataEditor.jsx +1 -202
- package/client/src/DataTable.jsx +1 -1
- package/client/src/constants.js +1 -1
- package/package.json +1 -1
- package/src/constants.js +2 -2
- package/src/defaultModels.js +1 -14
- package/src/email.js +7 -5
- package/src/engine.js +2 -1
- package/src/events.js +1 -1
- package/src/filter.js +221 -0
- package/src/modules/data.js +9 -9
- package/src/modules/workflow.js +266 -100
- package/src/packs.js +245 -7
- package/test/data.backup.integration.test.js +4 -1
- package/test/data.integration.test.js +10 -4
- package/test/file.test.js +11 -14
- package/test/import_export.integration.test.js +23 -19
- package/test/model.integration.test.js +20 -19
- package/test/user.test.js +30 -23
- package/test/vm.test.js +51 -0
- package/test/workflow.integration.test.js +6 -1
- package/test/workflow.robustness.test.js +6 -1
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 {
|
|
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
|
-
|
|
50
|
-
|
|
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 '
|
|
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(
|
|
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
|
-
},
|
|
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(
|
|
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")},
|
|
191
|
+
const result = await importData({ model: impexTestModel.name }, {file: blobToFile(blob,"test.csv")}, mockUser);
|
|
189
192
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
expect(result.
|
|
193
|
-
|
|
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(
|
|
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('
|
|
28
|
-
userPlan: '
|
|
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
|
|
138
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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 () => {
|