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.
- package/CONTRIBUTING.md +91 -0
- package/README.md +33 -17
- package/client/src/App.jsx +0 -5
- package/client/src/ConditionBuilder.scss +34 -1
- package/client/src/ConditionBuilder2.jsx +179 -53
- package/client/src/ContentView.jsx +0 -3
- package/client/src/CronBuilder.jsx +0 -1
- package/client/src/CronPartBuilder.jsx +0 -2
- package/client/src/DashboardView.jsx +0 -5
- package/client/src/DataEditor.jsx +8 -211
- package/client/src/DataLayout.jsx +0 -1
- package/client/src/DataTable.jsx +1 -3
- package/client/src/Field.jsx +0 -5
- package/client/src/FlexBuilder.jsx +1 -1
- package/client/src/ModelCreatorField.jsx +1 -5
- package/client/src/RTE.jsx +1 -6
- package/client/src/RTETrans.jsx +0 -2
- package/client/src/RelationField.jsx +1 -1
- package/client/src/RelationValue.jsx +1 -2
- package/client/src/TourSpotlight.jsx +0 -2
- package/client/src/constants.js +1 -1
- package/client/src/filter.js +87 -0
- package/client/src/hooks/data.js +1 -3
- package/client/src/hooks/useTutorials.jsx +0 -1
- package/package.json +3 -3
- package/server.js +2 -2
- package/src/constants.js +2 -2
- package/src/defaultModels.js +1 -14
- package/src/email.js +9 -7
- package/src/engine.js +60 -20
- package/src/events.js +1 -1
- package/src/filter.js +221 -0
- package/src/index.js +1 -1
- package/src/middlewares/middleware-mongodb.js +0 -1
- package/src/modules/assistant.js +1 -3
- package/src/modules/bucket.js +3 -4
- package/src/modules/{data.js → data/data.js} +42 -59
- package/src/modules/data/index.js +1 -0
- package/src/modules/file.js +1 -1
- package/src/modules/mongodb.js +0 -1
- package/src/modules/user.js +1 -1
- package/src/modules/workflow.js +299 -133
- package/src/packs.js +249 -8
- package/test/data.backup.integration.test.js +7 -5
- package/test/data.integration.test.js +8 -6
- package/test/events.test.js +1 -1
- package/test/file.test.js +11 -17
- package/test/import_export.integration.test.js +38 -27
- package/test/model.integration.test.js +20 -21
- package/test/user.test.js +32 -25
- package/test/vm.test.js +51 -0
- package/test/workflow.integration.test.js +22 -14
- package/test/workflow.robustness.test.js +19 -9
- 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 '
|
|
13
|
+
} from '../src/index.js';
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
modelsCollection as getAppModelsCollection,
|
|
17
|
-
getCollectionForUser as getAppUserCollection
|
|
18
|
-
} from '
|
|
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 '
|
|
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('
|
|
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 '
|
|
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 '
|
|
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
|
});
|
package/test/events.test.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
50
|
-
|
|
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 '
|
|
8
|
+
} from '../src/index.js';
|
|
10
9
|
|
|
11
10
|
import {
|
|
12
11
|
modelsCollection as getAppModelsCollection,
|
|
13
|
-
getCollectionForUser as getAppUserCollection
|
|
14
|
-
} from '
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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
|
-
},
|
|
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(
|
|
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")},
|
|
199
|
+
const result = await importData({ model: impexTestModel.name }, {file: blobToFile(blob,"test.csv")}, mockUser);
|
|
189
200
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
expect(result.
|
|
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(
|
|
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
|
});
|