data-primals-engine 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +440 -0
  2. package/Dockerfile +19 -9
  3. package/DockerfileTest +19 -9
  4. package/README.md +58 -24
  5. package/client/package-lock.json +172 -1
  6. package/client/package.json +1 -0
  7. package/client/src/App.scss +4 -0
  8. package/client/src/CalendarConfigModal.jsx +60 -21
  9. package/client/src/CalendarView.jsx +107 -0
  10. package/client/src/CalendarView.scss +9 -0
  11. package/client/src/DataLayout.jsx +54 -27
  12. package/client/src/DataTable.jsx +19 -2
  13. package/client/src/Dialog.scss +4 -3
  14. package/client/src/Field.jsx +3 -2
  15. package/client/src/HistoryDialog.jsx +253 -0
  16. package/client/src/HistoryDialog.scss +320 -0
  17. package/client/src/ModelCreator.jsx +36 -15
  18. package/client/src/ModelCreatorField.jsx +5 -6
  19. package/client/src/ViewSwitcher.jsx +6 -6
  20. package/client/src/hooks/useTutorials.jsx +1 -1
  21. package/client/src/translations.js +298 -1
  22. package/package.json +2 -2
  23. package/src/email.js +1 -1
  24. package/src/engine.js +6 -6
  25. package/src/events.js +15 -5
  26. package/src/gameObject.js +0 -29
  27. package/src/modules/data/data.history.js +490 -0
  28. package/src/modules/data/data.js +139 -108
  29. package/src/modules/data/data.routes.js +4 -4
  30. package/src/modules/mongodb.js +17 -1
  31. package/src/modules/user.js +9 -3
  32. package/src/packs.js +2 -1
  33. package/src/services/stripe.js +67 -13
  34. package/src/workers/crypto-worker.js +3 -3
  35. package/test/data.backup.integration.test.js +5 -0
  36. package/test/data.history.integration.test.js +193 -0
  37. package/test/data.integration.test.js +79 -39
  38. package/test/events.test.js +27 -27
  39. package/test/globalTeardown.js +1 -0
  40. package/test/model.integration.test.js +5 -0
  41. package/test/workflow.actions.integration.test.js +2 -0
  42. package/test/workflow.integration.test.js +2 -0
  43. package/test/workflow.robustness.test.js +2 -0
@@ -1,35 +1,25 @@
1
1
  import {Logger} from "../../gameObject.js";
2
2
  import {BSON, ObjectId} from "mongodb";
3
- import util, {promisify} from 'node:util';
3
+ import {promisify} from 'node:util';
4
4
  import crypto from "node:crypto";
5
- import {exec, execFile} from 'node:child_process';
5
+ import {execFile} from 'node:child_process';
6
6
  import sanitizeHtml from 'sanitize-html';
7
7
  import * as tar from "tar";
8
8
  import process from "node:process";
9
9
  import {randomColor} from "randomcolor";
10
10
  import cronstrue from 'cronstrue/i18n.js';
11
11
  import {mkdir} from 'node:fs/promises';
12
- import {
13
- anonymizeText,
14
- getDefaultForType,
15
- getFieldValueHash,
16
- getUserId,
17
- isDemoUser,
18
- isLocalUser
19
- } from "../../data.js";
12
+ import {onInit as historyInit} from "./data.history.js";
13
+ import {anonymizeText, getDefaultForType, getFieldValueHash, getUserId, isDemoUser, isLocalUser} from "../../data.js";
20
14
  import {
21
15
  allowedFields,
22
16
  dbName,
23
17
  install,
24
18
  maxAlertsPerUser,
25
- maxBytesPerSecondThrottleData,
26
19
  maxExportCount,
27
20
  maxFileSize,
28
21
  maxFilterDepth,
29
- maxMagnetsDataPerModel,
30
- maxMagnetsModels,
31
22
  maxModelNameLength,
32
- maxModelsPerUser,
33
23
  maxPasswordLength,
34
24
  maxPostData,
35
25
  maxRelationsPerData,
@@ -43,6 +33,7 @@ import {
43
33
  storageSafetyMargin
44
34
  } from "../../constants.js";
45
35
  import {
36
+ createCollection,
46
37
  getCollection,
47
38
  getCollectionForUser,
48
39
  getUserCollectionName,
@@ -52,7 +43,7 @@ import {
52
43
  } from "../mongodb.js";
53
44
  import {dbUrl, MongoClient, MongoDatabase} from "../../engine.js";
54
45
  import path from "node:path";
55
- import {getFileExtension, getObjectHash, getRandom, isGUID, isPlainObject, randomDate, sleep, uuidv4} from "../../core.js";
46
+ import {getObjectHash, getRandom, isGUID, isPlainObject, randomDate} from "../../core.js";
56
47
  import {Event} from "../../events.js";
57
48
  import fs from "node:fs";
58
49
  import schedule from "node-schedule";
@@ -67,12 +58,9 @@ import {
67
58
  import NodeCache from "node-cache";
68
59
  import AWS from 'aws-sdk';
69
60
  import checkDiskSpace from "check-disk-space";
70
- import {addFile, removeFile} from "../file.js";
61
+ import {addFile, removeFile} from "../file.js";
71
62
  import {downloadFromS3, getUserS3Config, listS3Backups, uploadToS3} from "../bucket.js";
72
- import {
73
- calculateTotalUserStorageUsage,
74
- hasPermission, middlewareAuthenticator
75
- } from "../user.js";
63
+ import {calculateTotalUserStorageUsage, hasPermission, middlewareAuthenticator} from "../user.js";
76
64
  import {getAllPacks} from "../../packs.js";
77
65
  import {Config} from "../../config.js";
78
66
  import {profiles} from "../../../client/src/constants.js";
@@ -500,53 +488,8 @@ export const getAPILang = (langs) => {
500
488
  }
501
489
 
502
490
 
503
- export function validateModelStructure(modelStructure) {
504
-
505
- const objectKeys = Object.keys(modelStructure);
506
- if( objectKeys.find(o => !["name", "_user", "locked", "_id", "description", "maxRequestData", "fields"].includes(o)) ){
507
- throw new Error(i18n.t('api.model.invalidStructure'));
508
- }
509
-
510
- // Vérification du type de name
511
- if (typeof modelStructure.name !== 'string' || !modelStructure.name) {
512
- throw new Error(i18n.t("api.validate.requiredFieldString", ["name"]));
513
- }
514
-
515
- // Vérification du type de description
516
- if (typeof modelStructure.description !== 'string') {
517
- throw new Error(i18n.t("api.validate.fieldString", ["description"]));
518
- }
519
-
520
- // Vérification de la présence et du type du tableau fields
521
- if (!Array.isArray(modelStructure.fields)) {
522
- throw new Error(i18n.t('api.validate.fieldArray', ["fields"]));
523
- }
524
-
525
- // Vérification de chaque champ dans le tableau fields
526
- for (const field of modelStructure.fields) {
527
- validateField(field);
528
- }
529
-
530
- if (modelStructure.constraints) {
531
- if (!Array.isArray(modelStructure.constraints)) {
532
- throw new Error('Model "constraints" property must be an array.');
533
- }
534
- const fieldNames = new Set(modelStructure.fields.map(f => f.name));
535
- for (const constraint of modelStructure.constraints) {
536
- if (constraint.type === 'unique') {
537
- if (!constraint.name || !Array.isArray(constraint.keys) || constraint.keys.length === 0) {
538
- throw new Error('Unique constraint must have a "name" and a non-empty "keys" array.');
539
- }
540
- for (const key of constraint.keys) {
541
- if (!fieldNames.has(key)) {
542
- throw new Error(`Constraint key "${key}" in constraint "${constraint.name}" does not exist as a field in the model.`);
543
- }
544
- }
545
- }
546
- }
547
- }
548
-
549
- return true; // La structure du modèle est valide
491
+ export async function validateModelStructure(modelStructure) {
492
+ return await Event.Trigger("OnValidateModelStructure", "event", "system", modelStructure);
550
493
  }
551
494
 
552
495
  const validateField = (field) => {
@@ -905,7 +848,7 @@ async function runStatefulAlertJob(alertId) {
905
848
  timestamp: new Date().toISOString(),
906
849
  message: `Alerte '${alertDoc.name}': ${count} élément(s) correspondent à votre condition.`
907
850
  };
908
- sendSseToUser(alertDoc._user, alertPayload);
851
+ await sendSseToUser(alertDoc._user, alertPayload);
909
852
 
910
853
  // Update state in DB to prevent re-notification
911
854
  await datasCollection.updateOne(
@@ -1030,7 +973,7 @@ export const editModel = async (user, id, data) => {
1030
973
  const dataModel = data;
1031
974
  try {
1032
975
  const collection = await getCollectionForUser(user);
1033
- validateModelStructure(dataModel);
976
+ await validateModelStructure(dataModel);
1034
977
 
1035
978
  const el = await modelsCollection.findOne({ $and: [
1036
979
  {_user: {$exists: true}},
@@ -1119,8 +1062,8 @@ export const editModel = async (user, id, data) => {
1119
1062
 
1120
1063
  const newModel = await modelsCollection.findOne({_id : oid});
1121
1064
  const res = ({ success: true, data: newModel });
1122
- const plugin = Event.Trigger("OnModelEdited", "event", "system", engine, newModel);
1123
- Event.Trigger("OnModelEdited", "event", "user", plugin?.data || newModel);
1065
+ const plugin = await Event.Trigger("OnModelEdited", "event", "system", engine, newModel);
1066
+ await Event.Trigger("OnModelEdited", "event", "user", plugin?.data || newModel);
1124
1067
  return plugin || res
1125
1068
  } catch (e) {
1126
1069
  logger.error(e);
@@ -1234,12 +1177,13 @@ export async function onInit(defaultEngine) {
1234
1177
 
1235
1178
  engine.use(middleware({ whitelist: mongoDBWhitelist }));
1236
1179
 
1237
- let modelsCollection, datasCollection, filesCollection, packsCollection, magnetsCollection;
1180
+ let modelsCollection, datasCollection, filesCollection, packsCollection, magnetsCollection, historyCollection;
1238
1181
 
1239
1182
  if( install ) {
1240
- datasCollection = await MongoDatabase.createCollection("datas");
1241
- filesCollection = await MongoDatabase.createCollection("files");
1242
- packsCollection = await MongoDatabase.createCollection("packs");
1183
+ datasCollection = await createCollection("datas");
1184
+ historyCollection = await createCollection("history");
1185
+ filesCollection = await createCollection("files");
1186
+ packsCollection = await createCollection("packs");
1243
1187
  //data
1244
1188
  const indexes = await datasCollection.indexes();
1245
1189
  if (!indexes.find(i => i.name === 'genericPartialIndex')) {
@@ -1265,7 +1209,7 @@ export async function onInit(defaultEngine) {
1265
1209
  await datasCollection.createIndex({_model: 1, _user: 1}, { name: 'modelUserIndex'});
1266
1210
  }
1267
1211
 
1268
- const jobsCollection = await MongoDatabase.createCollection("job_locks");
1212
+ const jobsCollection = await createCollection("job_locks");
1269
1213
  if (! await jobsCollection.indexExists("jobTTLIndex") ) {
1270
1214
  await jobsCollection.createIndex({ "lockedUntil": 1 }, { name: "jobTTLIndex", expireAfterSeconds: 0 });
1271
1215
  }
@@ -1309,6 +1253,7 @@ export async function onInit(defaultEngine) {
1309
1253
  datasCollection = getCollection("datas");
1310
1254
  filesCollection = getCollection("files");
1311
1255
  packsCollection = getCollection("packs");
1256
+ historyCollection = getCollection("history");
1312
1257
  }
1313
1258
  await registerRoutes(engine);
1314
1259
  logger = engine.getComponent(Logger);
@@ -1325,6 +1270,61 @@ export async function onInit(defaultEngine) {
1325
1270
  });
1326
1271
  await scheduleAlerts();
1327
1272
 
1273
+ // Triggers
1274
+
1275
+ Event.Listen("OnValidateModelStructure", async (modelStructure) =>{
1276
+
1277
+ const objectKeys = Object.keys(modelStructure);
1278
+
1279
+ if( objectKeys.find(o => !["name", "_user", "history", "locked", "_id", "description", "maxRequestData", "fields"].includes(o)) ){
1280
+ throw new Error(i18n.t('api.model.invalidStructure'));
1281
+ }
1282
+
1283
+ // Vérification du type de name
1284
+ if (typeof modelStructure.name !== 'string' || !modelStructure.name) {
1285
+ throw new Error(i18n.t("api.validate.requiredFieldString", ["name"]));
1286
+ }
1287
+
1288
+ // Vérification du type de description
1289
+ if (typeof modelStructure.description !== 'string') {
1290
+ throw new Error(i18n.t("api.validate.fieldString", ["description"]));
1291
+ }
1292
+
1293
+ // Vérification de la présence et du type du tableau fields
1294
+ if (!Array.isArray(modelStructure.fields)) {
1295
+ throw new Error(i18n.t('api.validate.fieldArray', ["fields"]));
1296
+ }
1297
+
1298
+ // Vérification de chaque champ dans le tableau fields
1299
+ for (const field of modelStructure.fields) {
1300
+ validateField(field);
1301
+ }
1302
+
1303
+ if (modelStructure.constraints) {
1304
+ if (!Array.isArray(modelStructure.constraints)) {
1305
+ throw new Error('Model "constraints" property must be an array.');
1306
+ }
1307
+ const fieldNames = new Set(modelStructure.fields.map(f => f.name));
1308
+ for (const constraint of modelStructure.constraints) {
1309
+ if (constraint.type === 'unique') {
1310
+ if (!constraint.name || !Array.isArray(constraint.keys) || constraint.keys.length === 0) {
1311
+ throw new Error('Unique constraint must have a "name" and a non-empty "keys" array.');
1312
+ }
1313
+ for (const key of constraint.keys) {
1314
+ if (!fieldNames.has(key)) {
1315
+ throw new Error(`Constraint key "${key}" in constraint "${constraint.name}" does not exist as a field in the model.`);
1316
+ }
1317
+ }
1318
+ }
1319
+ }
1320
+ }
1321
+
1322
+ return true; // La structure du modèle est valide
1323
+ }, "event", "system");
1324
+
1325
+
1326
+ // Sub modules
1327
+ historyInit(defaultEngine);
1328
1328
  }
1329
1329
 
1330
1330
  export const createModel = async (data) => {
@@ -1533,7 +1533,7 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
1533
1533
  };
1534
1534
 
1535
1535
  // Envoyer l'alerte à l'utilisateur spécifique via SSE
1536
- const sent = sendSseToUser(doc._user, alertPayload);
1536
+ const sent = await sendSseToUser(doc._user, alertPayload);
1537
1537
 
1538
1538
  if (sent) {
1539
1539
  logger.info(`[Scheduled Job] Successfully sent SSE alert for job ${jobId} to user ${doc._user}.`);
@@ -1574,10 +1574,18 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
1574
1574
  // Attendre que toutes les opérations post-insertion (workflows, planification) soient tentées
1575
1575
  await Promise.allSettled(postInsertionPromises);
1576
1576
  }
1577
- const res = { success: true, insertedIds: insertedIds.map(id => id.toString()) }; // Convertir les IDs en string pour la réponse
1578
- const plugin = Event.Trigger("OnDataAdded", "event", "system", engine, insertedIds);
1579
- Event.Trigger("OnDataAdded", "event", "user", plugin?.insertedIds || insertedIds);
1580
- return plugin || res;
1577
+
1578
+ // System specific event
1579
+ const eventPayload = { modelName, insertedIds, user };
1580
+ await Event.Trigger("OnDataAdded", "event", "system", engine, eventPayload);
1581
+
1582
+ // User specific event
1583
+ const userPayload = {...eventPayload};
1584
+ delete userPayload['user'];
1585
+ await Event.Trigger("OnDataAdded", "event", "user", userPayload);
1586
+
1587
+ // Return valid result
1588
+ return { success: true, insertedIds: insertedIds.map(id => id.toString()) }; // Convertir les IDs en string pour la réponse
1581
1589
 
1582
1590
  } catch (error) { // Attrape les erreurs de permission ou de pushDataUnsecure
1583
1591
  logger.error(`[insertData] Main error during insertion process for model ${modelName}: ${error.message}`, error.stack);
@@ -1717,7 +1725,7 @@ async function initializeAndValidate(data, modelName, me) {
1717
1725
 
1718
1726
  const model = await getModel(modelName, me);
1719
1727
  const collection = await getCollectionForUser(me);
1720
- validateModelStructure(model);
1728
+ await validateModelStructure(model);
1721
1729
 
1722
1730
  return { datas, model, collection };
1723
1731
  }
@@ -1780,7 +1788,7 @@ async function processDocuments(datas, model, collection, me) {
1780
1788
  const idMap = new Map();
1781
1789
  const allInsertedIds = [];
1782
1790
 
1783
- const realData = Event.Trigger("OnDataInsert", "event", "system", datas) || datas;
1791
+ const realData = await Event.Trigger("OnDataInsert", "event", "system", datas) || datas;
1784
1792
  for (const doc of realData) {
1785
1793
  try {
1786
1794
  const newDocId = await insertAndResolveRelations(doc, model, collection, me, idMap);
@@ -1986,7 +1994,7 @@ async function applyFieldFilters(docToProcess, model) {
1986
1994
  docToProcess[field.name],
1987
1995
  field
1988
1996
  );
1989
- const realFilter = Event.Trigger('OnDataFilter', "event", "system",filter, field, docToProcess );
1997
+ const realFilter = await Event.Trigger('OnDataFilter', "event", "system",filter, field, docToProcess );
1990
1998
  docToProcess[field.name] = realFilter || filter;
1991
1999
  }
1992
2000
  }
@@ -1996,7 +2004,7 @@ async function applyFieldFilters(docToProcess, model) {
1996
2004
  /**
1997
2005
  * Valide la structure et le contenu du document selon le modèle
1998
2006
  */
1999
- function validateModelData(doc, model, isPatch = false) {
2007
+ async function validateModelData(doc, model, isPatch = false) {
2000
2008
  if (!isPatch) {
2001
2009
  model.fields.forEach(field => {
2002
2010
  const value = doc[field.name];
@@ -2018,7 +2026,7 @@ function validateModelData(doc, model, isPatch = false) {
2018
2026
 
2019
2027
  const validator = dataTypes[fieldDef.type]?.validate;
2020
2028
  const valid = validator && validator(value, fieldDef);
2021
- const realValidation = Event.Trigger('OnDataValidate', "event", "system", value, fieldDef,doc );
2029
+ const realValidation = await Event.Trigger('OnDataValidate', "event", "system", value, fieldDef,doc );
2022
2030
  if (!(valid || realValidation)) {
2023
2031
  throw new Error(i18n.t('api.field.validationFailed', { field: fieldName, value }));
2024
2032
  }
@@ -2055,9 +2063,9 @@ async function insertAndResolveRelations(doc, model, collection, me, idMap) {
2055
2063
  docToProcess._id = new ObjectId(originalId);
2056
2064
  }
2057
2065
 
2058
- validateModelData(docToProcess, model);
2066
+ await validateModelData(docToProcess, model);
2059
2067
  await processRelations(docToProcess, model, collection, me, idMap);
2060
- validateModelData(docToProcess, model);
2068
+ await validateModelData(docToProcess, model);
2061
2069
  await applyFieldFilters(docToProcess, model);
2062
2070
  await checkUniqueFields(docToProcess, model, collection);
2063
2071
 
@@ -2299,9 +2307,9 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
2299
2307
  // 4. Validation adaptée pour patch ou edit (inchangé)
2300
2308
  if (!isPatch) {
2301
2309
  const dataToValidate = { ...existingDocs[0], ...updateData };
2302
- validateModelData(dataToValidate, model, false);
2310
+ await validateModelData(dataToValidate, model, false);
2303
2311
  } else {
2304
- validateModelData(updateData, model, true);
2312
+ await validateModelData(updateData, model, true);
2305
2313
  }
2306
2314
 
2307
2315
  // 5. Vérification des champs uniques (inchangé)
@@ -2325,9 +2333,21 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
2325
2333
  for (const field of relationFields) {
2326
2334
  if (updateData[field.name] !== undefined) {
2327
2335
  const relationValue = updateData[field.name];
2328
- if (relationValue !== null && typeof relationValue === 'object') {
2329
- const insertedIds = await pushDataUnsecure(relationValue, field.relation, user, { preserveIds: true });
2330
- updateData[field.name] = field.multiple ? insertedIds || [] : insertedIds?.[0] || null;
2336
+ // Only process relations if the value is an object or an array containing at least one object.
2337
+ // An array of strings (ObjectIDs) should be passed through as-is for the update.
2338
+ let shouldProcessRelation = false;
2339
+ if (Array.isArray(relationValue)) {
2340
+ // If any item in the array is a plain object, we need to process the whole array
2341
+ // to handle potential nested creations or lookups.
2342
+ if (relationValue.some(item => isPlainObject(item))) {
2343
+ shouldProcessRelation = true;
2344
+ }
2345
+ } else if (isPlainObject(relationValue)) {
2346
+ shouldProcessRelation = true;
2347
+ }
2348
+ if (shouldProcessRelation) {
2349
+ const insertedIds = await pushDataUnsecure(relationValue, field.relation, user);
2350
+ updateData[field.name] = field.multiple ? insertedIds || [] : (insertedIds?.[0] || null);
2331
2351
  }
2332
2352
  }
2333
2353
  }
@@ -2402,6 +2422,17 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
2402
2422
  const bulkResult = await collection.bulkWrite(bulkOps);
2403
2423
  const modifiedCount = bulkResult.modifiedCount || 0;
2404
2424
 
2425
+ // Déclencher l'événement OnDataEdited avec les états avant/après
2426
+ if (modifiedCount > 0) {
2427
+ const updatedDocs = await collection.find({ _id: { $in: ids } }).toArray();
2428
+ await Event.Trigger("OnDataEdited", "event", "system", engine, {
2429
+ modelName,
2430
+ user,
2431
+ before: existingDocs, // Documents avant la modification
2432
+ after: updatedDocs // Documents après la modification
2433
+ });
2434
+ }
2435
+
2405
2436
  // 11. Tâches post-mise à jour (schedules, workflows) (inchangé)
2406
2437
  if (["workflowTrigger", "alert"].includes(modelName)) {
2407
2438
  await handleScheduledJobs(modelName, existingDocs, collection, finalDataForSet);
@@ -2462,7 +2493,7 @@ async function handleScheduledJobs(modelName, existingDocs, collection, updateDa
2462
2493
  schedule.scheduleJob(jobId, updatedDoc.cronExpression, async () => {
2463
2494
  logger.info(`[Scheduled Job] Cron triggered for job ${jobId}`);
2464
2495
  await runScheduledJobWithDbLock(jobId, async () => {
2465
- sendSseToUser(updatedDoc._user, {
2496
+ await sendSseToUser(updatedDoc._user, {
2466
2497
  type: 'cron_alert',
2467
2498
  triggerId: updatedDoc._id.toString(),
2468
2499
  triggerName: updatedDoc.name,
@@ -2685,8 +2716,8 @@ export const deleteData = async (modelName, filter, user ={}, triggerWorkflow, w
2685
2716
  }
2686
2717
 
2687
2718
  const res = { success: true, deletedCount }
2688
- const plugin = Event.Trigger("OnDataDeleted", "event", "system", engine, {model:modelName, filter});
2689
- Event.Trigger("OnDataDeleted", "event", "user", {model:modelName, filter});
2719
+ const plugin = await Event.Trigger("OnDataDeleted", "event", "system", engine, {model:modelName, filter});
2720
+ await Event.Trigger("OnDataDeleted", "event", "user", {model:modelName, filter});
2690
2721
  return plugin || res;
2691
2722
 
2692
2723
  } catch (error) {
@@ -3194,8 +3225,8 @@ export const searchData = async (query, user) => {
3194
3225
  data = await handleFields(modelElement, data, user);
3195
3226
 
3196
3227
  const res = {data, count: count[0]?.count || 0};
3197
- const plugin = Event.Trigger("OnDataSearched", "event", "system", engine, {data, count: count[0]?.count});
3198
- Event.Trigger("OnDataSearched", "event", "user", plugin || {data, count: count[0]?.count});
3228
+ const plugin = await Event.Trigger("OnDataSearched", "event", "system", engine, {data, count: count[0]?.count});
3229
+ await Event.Trigger("OnDataSearched", "event", "user", plugin || {data, count: count[0]?.count});
3199
3230
  return plugin || res;
3200
3231
  }
3201
3232
 
@@ -3272,7 +3303,7 @@ export const importData = async(options, files, user) => {
3272
3303
  });
3273
3304
  }
3274
3305
 
3275
- validateModelStructure(modelData);
3306
+ await validateModelStructure(modelData);
3276
3307
  await modelsCollection.insertOne(modelData);
3277
3308
  logger.info(`[Model Import] Successfully imported model '${modelName}' for user ${user.username}.`);
3278
3309
  } catch (e) {
@@ -3354,7 +3385,7 @@ export const importData = async(options, files, user) => {
3354
3385
  if (insertedIdsArray && insertedIdsArray.length > 0) {
3355
3386
  importJobs[importJobId].processedRecords += insertedIdsArray.length;
3356
3387
  logger.debug(`[Import Job ${importJobId}] Processed chunk for '${modelName}': ${insertedIdsArray.length} records. Total processed: ${importJobs[importJobId].processedRecords}`);
3357
- sendSseToUser(user.username, {
3388
+ await sendSseToUser(user.username, {
3358
3389
  type: 'import_progress',
3359
3390
  job: importJobs[importJobId]
3360
3391
  });
@@ -3458,7 +3489,7 @@ export const importData = async(options, files, user) => {
3458
3489
  const insertedIdsArray = await pushDataUnsecure(chunk, modelNameForImport, user, {});
3459
3490
  if (insertedIdsArray && insertedIdsArray.length > 0) {
3460
3491
  importJobs[importJobId].processedRecords += insertedIdsArray.length;
3461
- sendSseToUser(user.username, {
3492
+ await sendSseToUser(user.username, {
3462
3493
  type: 'import_progress',
3463
3494
  job: importJobs[importJobId]
3464
3495
  });
@@ -3552,7 +3583,7 @@ export const importData = async(options, files, user) => {
3552
3583
  const insertedIdsArray = await pushDataUnsecure(chunk, modelNameForImport, user, {});
3553
3584
  if (insertedIdsArray && insertedIdsArray.length > 0) {
3554
3585
  importJobs[importJobId].processedRecords += insertedIdsArray.length;
3555
- sendSseToUser(user.username, {
3586
+ await sendSseToUser(user.username, {
3556
3587
  type: 'import_progress',
3557
3588
  job: importJobs[importJobId]
3558
3589
  });
@@ -3597,7 +3628,7 @@ export const importData = async(options, files, user) => {
3597
3628
  } else {
3598
3629
  importJobs[importJobId].status = 'completed';
3599
3630
  }
3600
- sendSseToUser(user.username, {
3631
+ await sendSseToUser(user.username, {
3601
3632
  type: 'import_progress',
3602
3633
  job: importJobs[importJobId]
3603
3634
  });
@@ -3733,8 +3764,8 @@ export const exportData= async (options, user) =>{
3733
3764
  }
3734
3765
 
3735
3766
  const res = { success: true, data: exportResults, models: modelsToExport };
3736
- const plugin = Event.Trigger("OnDataExported", "event", "system", engine, exportResults, modelsToExport);
3737
- Event.Trigger("OnDataExported", "event", "user", plugin?.exportResults || exportResults, plugin?.modelsToExport || modelsToExport);
3767
+ const plugin = await Event.Trigger("OnDataExported", "event", "system", engine, exportResults, modelsToExport);
3768
+ await Event.Trigger("OnDataExported", "event", "user", plugin?.exportResults || exportResults, plugin?.modelsToExport || modelsToExport);
3738
3769
  return plugin || res;
3739
3770
  }
3740
3771
 
@@ -4086,7 +4117,7 @@ export const loadFromDump = async (user, options = {}) => {
4086
4117
  modelsCache.flushAll();
4087
4118
 
4088
4119
  logger.info(`[${action}] Restore successful for user ${user.username}.`);
4089
- Event.Trigger("OnDataRestored", "event", "system");
4120
+ await Event.Trigger("OnDataRestored", "event", "system");
4090
4121
 
4091
4122
  } finally {
4092
4123
  // --- GUARANTEED CLEANUP ---
@@ -4356,7 +4387,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
4356
4387
  preparedModel.fields.forEach(f => f.locked = false);
4357
4388
  }
4358
4389
 
4359
- validateModelStructure(preparedModel);
4390
+ await validateModelStructure(preparedModel);
4360
4391
  await modelsCollection.insertOne(preparedModel);
4361
4392
  summary.models.installed.push(modelName);
4362
4393
 
@@ -4527,7 +4558,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', isTe
4527
4558
 
4528
4559
  // Trigger event only if pack came from database (original behavior)
4529
4560
  if (typeof packIdentifier === 'string') {
4530
- Event.Trigger("OnPackInstalled", "event", "system", pack);
4561
+ await Event.Trigger("OnPackInstalled", "event", "system", pack);
4531
4562
  }
4532
4563
 
4533
4564
  const modifiedCount = summary.datas.inserted + summary.datas.updated;
@@ -144,10 +144,10 @@ const middlewareLogger = async (req, res, next) => {
144
144
  * @param {object} data - L'objet de données à envoyer.
145
145
  * @returns {boolean} - True si l'événement a été envoyé, false sinon.
146
146
  */
147
- export function sendSseToUser(username, data) {
147
+ export async function sendSseToUser(username, data) {
148
148
  const res = sseConnections.get(username);
149
149
  if (res) {
150
- const ssePlugin = Event.Trigger("sendSseToUser", "system", "calls", data) || data;
150
+ const ssePlugin = await Event.Trigger("sendSseToUser", "system", "calls", data) || data;
151
151
  res.write(`data: ${JSON.stringify(ssePlugin)}\n\n`);
152
152
  return true;
153
153
  }
@@ -791,7 +791,7 @@ export async function registerRoutes(engine){
791
791
  }
792
792
  try {
793
793
  const modelData = req.fields;
794
- validateModelStructure(modelData);
794
+ await validateModelStructure(modelData);
795
795
 
796
796
 
797
797
  const existingModel = await modelsCollection.findOne({name: modelData.name, $and: [{_user: {$exists: true}}, {$or: [{_user: req.me._user}, {_user: req.me.username}]}] });
@@ -872,7 +872,7 @@ export async function registerRoutes(engine){
872
872
  await cancelAlerts(req.me);
873
873
 
874
874
  await getPromise();
875
- Event.Trigger('jobAddUserData', req.me.username);
875
+ await Event.Trigger('jobAddUserData', req.me.username);
876
876
  }else{
877
877
  await getPromise();
878
878
  }
@@ -8,6 +8,7 @@ export let modelsCollection, datasCollection, filesCollection, packsCollection;
8
8
  export {ObjectId};
9
9
 
10
10
  let engine, logger;
11
+ let colls= [];
11
12
  export async function onInit(defaultEngine) {
12
13
  engine = defaultEngine;
13
14
  logger = engine.getComponent(Logger);
@@ -17,9 +18,24 @@ export async function onInit(defaultEngine) {
17
18
  filesCollection = getCollection("files");
18
19
  packsCollection = getCollection("packs");
19
20
 
21
+ colls = await MongoDatabase.listCollections().toArray();
22
+
20
23
  logger.info("MongoDB collections loaded.");
21
- };
24
+ }
22
25
 
26
+ export const getCollections= async (forceRefresh)=>{
27
+ if( !forceRefresh )
28
+ return colls;
29
+ colls = await MongoDatabase.listCollections().toArray();
30
+ }
31
+
32
+ export const createCollection = async (coll)=>{
33
+ const found =colls.find(f => f.name === coll);
34
+ if( found){
35
+ return getCollection(coll);
36
+ }
37
+ return await MongoDatabase.createCollection(coll);
38
+ }
23
39
 
24
40
  export const isObjectId = (id) => {
25
41
  return (typeof(id) === 'string' && id.match(/^[0-9a-fA-F]{24}$/));
@@ -1,6 +1,12 @@
1
1
  import i18n from "../i18n.js";
2
2
  import {MongoDatabase} from "../engine.js";
3
- import {getCollection, getCollectionForUser, getUserCollectionName} from "./mongodb.js";
3
+ import {
4
+ createCollection,
5
+ getCollection,
6
+ getCollectionForUser,
7
+ getCollections,
8
+ getUserCollectionName
9
+ } from "./mongodb.js";
4
10
  import {isLocalUser} from "../data.js";
5
11
  import {ObjectId} from "mongodb";
6
12
  import {getAPILang, searchData} from "./data/index.js";
@@ -21,11 +27,11 @@ export const userInitiator = async (req, res, next) => {
21
27
  i18n.changeLanguage(lang);
22
28
 
23
29
  if (await engine.userProvider.hasFeature(req.me, 'indexes')) {
24
- const collections = await MongoDatabase.listCollections().toArray();
30
+ const collections = await getCollections();
25
31
  const collectionNames = collections.map(c => c.name);
26
32
  const coll = await getUserCollectionName(req.me);
27
33
  if (collectionNames.includes(coll)) {
28
- const collection = await MongoDatabase.createCollection(coll);
34
+ const collection = await createCollection(coll);
29
35
  const indexes = await collection.indexes();
30
36
  if (!indexes.find(i => i.name === 'genericPartialIndex')) {
31
37
  await collection.createIndex({"$**": 1}, {
package/src/packs.js CHANGED
@@ -4259,7 +4259,8 @@ This ensures that your application only processes legitimate requests from Strip
4259
4259
  "all": {
4260
4260
  "env": [
4261
4261
  { "name": "STRIPE_SECRET_KEY", "value": "sk_test_YOUR_SECRET_KEY" },
4262
- { "name": "STRIPE_WEBHOOK_SECRET", "value": "whsec_YOUR_WEBHOOK_SECRET" }
4262
+ { "name": "STRIPE_WEBHOOK_SECRET", "value": "whsec_YOUR_WEBHOOK_SECRET" },
4263
+ { "name": "APP_BASE_URL", "value": "https://yourdomain.tld" }
4263
4264
  ],
4264
4265
  "endpoint": [
4265
4266
  {