data-primals-engine 1.2.6 → 1.3.1

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 (56) hide show
  1. package/CHANGELOG.md +440 -0
  2. package/README.md +782 -712
  3. package/client/src/App.jsx +13 -20
  4. package/client/src/App.scss +4 -0
  5. package/client/src/AssistantChat.jsx +5 -4
  6. package/client/src/DataEditor.jsx +1 -1
  7. package/client/src/DataLayout.jsx +2 -2
  8. package/client/src/DataTable.jsx +66 -5
  9. package/client/src/ExportDialog.jsx +2 -2
  10. package/client/src/Field.jsx +5 -4
  11. package/client/src/HistoryDialog.jsx +96 -0
  12. package/client/src/HistoryDialog.scss +73 -0
  13. package/client/src/KanbanCard.jsx +4 -2
  14. package/client/src/KanbanConfigModal.jsx +5 -7
  15. package/client/src/ModelCreator.jsx +36 -15
  16. package/client/src/ModelCreatorField.jsx +14 -15
  17. package/client/src/PackGallery.jsx +89 -9
  18. package/client/src/PackGallery.scss +58 -4
  19. package/client/src/RTETrans.jsx +11 -0
  20. package/client/src/RelationValue.jsx +3 -4
  21. package/client/src/constants.js +1 -1
  22. package/client/src/core/data.js +2 -1
  23. package/client/src/hooks/useTutorials.jsx +1 -1
  24. package/client/src/translations.js +217 -0
  25. package/package.json +9 -2
  26. package/server.js +4 -4
  27. package/src/HistoryDialog.jsx +86 -0
  28. package/src/HistoryDialog.scss +77 -0
  29. package/src/constants.js +6 -0
  30. package/src/defaultModels.js +23 -10
  31. package/src/email.js +1 -1
  32. package/src/engine.js +6 -6
  33. package/src/events.js +15 -5
  34. package/src/gameObject.js +0 -29
  35. package/src/i18n.js +1 -1
  36. package/src/modules/{assistant.js → assistant/assistant.js} +42 -27
  37. package/src/modules/assistant/constants.js +16 -0
  38. package/src/modules/data/data.history.js +304 -0
  39. package/src/modules/data/data.js +306 -238
  40. package/src/modules/data/data.routes.js +33 -7
  41. package/src/modules/mongodb.js +17 -1
  42. package/src/modules/user.js +21 -4
  43. package/src/modules/workflow.js +133 -48
  44. package/src/packs.js +1016 -9
  45. package/src/services/index.js +11 -0
  46. package/src/services/stripe.js +195 -0
  47. package/src/workers/crypto-worker.js +3 -3
  48. package/test/data.backup.integration.test.js +5 -0
  49. package/test/data.history.integration.test.js +193 -0
  50. package/test/data.integration.test.js +144 -41
  51. package/test/events.test.js +27 -27
  52. package/test/globalTeardown.js +1 -0
  53. package/test/model.integration.test.js +5 -0
  54. package/test/workflow.actions.integration.test.js +4 -2
  55. package/test/workflow.integration.test.js +3 -1
  56. 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
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";
@@ -193,7 +181,7 @@ export const dataTypes = {
193
181
  }
194
182
  return value;
195
183
  },
196
- anonymize: anonymizeText
184
+ anonymize: (str) => anonymizeText(typeof(str) ==='object' ? JSON.stringify(str): str)
197
185
  },
198
186
  richtext: {
199
187
  validate: (value, field) => {
@@ -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);
@@ -1129,38 +1072,71 @@ export const editModel = async (user, id, data) => {
1129
1072
  };
1130
1073
 
1131
1074
 
1132
- export async function handleCustomEndpointRequest(req, res) {
1075
+ export async function middlewareEndpointAuthenticator(req, res, next) {
1133
1076
  const { path } = req.params;
1134
1077
  const method = req.method.toUpperCase();
1135
-
1136
- const user = req.me;
1137
- if (!user) {
1138
- return res.status(401).json({ success: false, message: 'Authentication required.' });
1139
- }
1078
+ const datasCollection = getCollection('datas');
1140
1079
 
1141
1080
  try {
1142
- // 1. Trouver l'endpoint correspondant dans la base de données
1143
- const endpointSearch = await searchData({
1144
- model: 'endpoint',
1145
- filter: {
1146
- path: path,
1147
- method: method,
1148
- isActive: true
1149
- },
1150
- limit: 1
1151
- }, user);
1081
+ const endpointDef = await datasCollection.findOne({
1082
+ _model: 'endpoint',
1083
+ path: path,
1084
+ method: method,
1085
+ isActive: true
1086
+ });
1152
1087
 
1153
- if (endpointSearch.count === 0) {
1154
- logger.warn(`[Endpoint] 404 - No active endpoint found for user '${user.username}', path '${path}', method '${method}'.`);
1088
+ if (!endpointDef) {
1155
1089
  return res.status(404).json({ success: false, message: 'Endpoint not found.' });
1156
1090
  }
1157
1091
 
1158
- const endpointDef = endpointSearch.data[0];
1092
+ // Attacher la définition à la requête pour que le handler suivant puisse l'utiliser
1093
+ req.endpointDef = endpointDef;
1094
+
1095
+ // Si l'endpoint n'est PAS public, on exécute le vrai middleware d'authentification
1096
+ if (!endpointDef.isPublic) {
1097
+ // On "chaîne" vers le middleware authenticator standard.
1098
+ // Il se chargera de vérifier le token et de renvoyer une 401 si nécessaire.
1099
+ return middlewareAuthenticator(req, res, next);
1100
+ }
1101
+
1102
+ // Si l'endpoint EST public, on passe simplement à la suite.
1103
+ next();
1104
+
1105
+ } catch (error) {
1106
+ logger.error(`[EndpointAuth] Critical error: ${error.message}`, error.stack);
1107
+ res.status(500).json({ success: false, message: 'Internal server error during endpoint authentication.' });
1108
+ }
1109
+ }
1110
+
1111
+ export async function handleCustomEndpointRequest(req, res) {
1112
+ const endpointDef = req.endpointDef;
1113
+
1114
+ try {
1115
+ let executionUser = null;
1116
+
1117
+ // 1. Déterminer le contexte utilisateur pour l'exécution
1118
+ if (endpointDef.isPublic) {
1119
+ // Pour les endpoints publics, on exécute le script en tant que propriétaire de l'endpoint.
1120
+ if (!endpointDef._user) {
1121
+ logger.error(`[Endpoint] Misconfiguration: Public endpoint '${endpointDef.name}' (ID: ${endpointDef._id}) has no owner.`);
1122
+ return res.status(500).json({ success: false, message: 'Endpoint misconfigured: owner missing.' });
1123
+ }
1124
+ executionUser = await engine.userProvider.findUserByUsername(endpointDef._user);
1125
+ if (!executionUser) {
1126
+ logger.error(`[Endpoint] Execution failed: Owner '${endpointDef._user}' for public endpoint '${endpointDef.name}' not found.`);
1127
+ return res.status(500).json({ success: false, message: 'Endpoint owner not found.' });
1128
+ }
1129
+ logger.info(`[Endpoint] Public endpoint '${endpointDef.name}' running as owner '${executionUser.username}'.`);
1130
+ } else {
1131
+ // Pour les endpoints privés, l'utilisateur a déjà été authentifié par le middleware.
1132
+ // req.me est garanti d'exister ici.
1133
+ executionUser = req.me;
1134
+ logger.info(`[Endpoint] Private endpoint '${endpointDef.name}' running as authenticated user '${executionUser.username}'.`);
1135
+ }
1159
1136
 
1160
1137
  // 2. Préparer le contexte pour le script
1161
- // On donne au script accès au corps, aux paramètres de la requête, etc.
1162
1138
  const contextData = {
1163
- request:{
1139
+ request: {
1164
1140
  body: req.fields,
1165
1141
  query: req.query,
1166
1142
  params: req.params,
@@ -1168,34 +1144,29 @@ export async function handleCustomEndpointRequest(req, res) {
1168
1144
  }
1169
1145
  };
1170
1146
 
1171
- // 3. Exécuter le code de l'endpoint en utilisant notre sandbox sécurisé
1172
- logger.info(`[Endpoint] Executing endpoint '${endpointDef.name}' for user '${user.username}'.`);
1147
+ // 3. Exécuter le code de l'endpoint
1173
1148
  const result = await executeSafeJavascript(
1174
- { script: endpointDef.code }, // On passe la définition du script
1149
+ { script: endpointDef.code },
1175
1150
  contextData,
1176
- user
1151
+ executionUser // Use the determined user for execution
1177
1152
  );
1178
1153
 
1179
1154
  // 4. Envoyer la réponse
1180
1155
  if (result.success) {
1181
- // Le script a réussi, on retourne sa sortie
1182
1156
  res.status(200).json(result.data);
1183
1157
  } else {
1184
- // Le script a échoué, on retourne une erreur 500 avec les logs
1185
1158
  logger.error(`[Endpoint] Execution failed for '${endpointDef.name}'. Error: ${result.message}`);
1186
-
1187
- const r = {
1159
+ const responseError = {
1188
1160
  success: false,
1189
1161
  message: 'Endpoint script execution failed.',
1190
- // On peut choisir d'exposer les logs pour le débogage
1191
- details: result.message
1162
+ details: result.message,
1163
+ logs: result.logs
1192
1164
  };
1193
- r.logs = result.logs;
1194
- res.status(500).json(r);
1165
+ res.status(500).json(responseError);
1195
1166
  }
1196
1167
 
1197
1168
  } catch (error) {
1198
- logger.error(`[Endpoint] Critical error handling request for path '${path}': ${error.message}`, process.env.NODE_ENV === 'development'? error.stack : error.stack[0]);
1169
+ logger.error(`[Endpoint] Critical error handling request for path '${endpointDef.path}': ${error.message}`, error.stack);
1199
1170
  res.status(500).json({ success: false, message: 'An internal server error occurred.' });
1200
1171
  }
1201
1172
  }
@@ -1206,12 +1177,13 @@ export async function onInit(defaultEngine) {
1206
1177
 
1207
1178
  engine.use(middleware({ whitelist: mongoDBWhitelist }));
1208
1179
 
1209
- let modelsCollection, datasCollection, filesCollection, packsCollection, magnetsCollection;
1180
+ let modelsCollection, datasCollection, filesCollection, packsCollection, magnetsCollection, historyCollection;
1210
1181
 
1211
1182
  if( install ) {
1212
- datasCollection = await MongoDatabase.createCollection("datas");
1213
- filesCollection = await MongoDatabase.createCollection("files");
1214
- 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");
1215
1187
  //data
1216
1188
  const indexes = await datasCollection.indexes();
1217
1189
  if (!indexes.find(i => i.name === 'genericPartialIndex')) {
@@ -1237,7 +1209,7 @@ export async function onInit(defaultEngine) {
1237
1209
  await datasCollection.createIndex({_model: 1, _user: 1}, { name: 'modelUserIndex'});
1238
1210
  }
1239
1211
 
1240
- const jobsCollection = await MongoDatabase.createCollection("job_locks");
1212
+ const jobsCollection = await createCollection("job_locks");
1241
1213
  if (! await jobsCollection.indexExists("jobTTLIndex") ) {
1242
1214
  await jobsCollection.createIndex({ "lockedUntil": 1 }, { name: "jobTTLIndex", expireAfterSeconds: 0 });
1243
1215
  }
@@ -1281,6 +1253,7 @@ export async function onInit(defaultEngine) {
1281
1253
  datasCollection = getCollection("datas");
1282
1254
  filesCollection = getCollection("files");
1283
1255
  packsCollection = getCollection("packs");
1256
+ historyCollection = getCollection("history");
1284
1257
  }
1285
1258
  await registerRoutes(engine);
1286
1259
  logger = engine.getComponent(Logger);
@@ -1297,6 +1270,61 @@ export async function onInit(defaultEngine) {
1297
1270
  });
1298
1271
  await scheduleAlerts();
1299
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);
1300
1328
  }
1301
1329
 
1302
1330
  export const createModel = async (data) => {
@@ -1505,7 +1533,7 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
1505
1533
  };
1506
1534
 
1507
1535
  // Envoyer l'alerte à l'utilisateur spécifique via SSE
1508
- const sent = sendSseToUser(doc._user, alertPayload);
1536
+ const sent = await sendSseToUser(doc._user, alertPayload);
1509
1537
 
1510
1538
  if (sent) {
1511
1539
  logger.info(`[Scheduled Job] Successfully sent SSE alert for job ${jobId} to user ${doc._user}.`);
@@ -1546,10 +1574,18 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
1546
1574
  // Attendre que toutes les opérations post-insertion (workflows, planification) soient tentées
1547
1575
  await Promise.allSettled(postInsertionPromises);
1548
1576
  }
1549
- const res = { success: true, insertedIds: insertedIds.map(id => id.toString()) }; // Convertir les IDs en string pour la réponse
1550
- const plugin = Event.Trigger("OnDataAdded", "event", "system", engine, insertedIds);
1551
- Event.Trigger("OnDataAdded", "event", "user", plugin?.insertedIds || insertedIds);
1552
- 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
1553
1589
 
1554
1590
  } catch (error) { // Attrape les erreurs de permission ou de pushDataUnsecure
1555
1591
  logger.error(`[insertData] Main error during insertion process for model ${modelName}: ${error.message}`, error.stack);
@@ -1689,7 +1725,7 @@ async function initializeAndValidate(data, modelName, me) {
1689
1725
 
1690
1726
  const model = await getModel(modelName, me);
1691
1727
  const collection = await getCollectionForUser(me);
1692
- validateModelStructure(model);
1728
+ await validateModelStructure(model);
1693
1729
 
1694
1730
  return { datas, model, collection };
1695
1731
  }
@@ -1752,7 +1788,7 @@ async function processDocuments(datas, model, collection, me) {
1752
1788
  const idMap = new Map();
1753
1789
  const allInsertedIds = [];
1754
1790
 
1755
- const realData = Event.Trigger("OnDataInsert", "event", "system", datas) || datas;
1791
+ const realData = await Event.Trigger("OnDataInsert", "event", "system", datas) || datas;
1756
1792
  for (const doc of realData) {
1757
1793
  try {
1758
1794
  const newDocId = await insertAndResolveRelations(doc, model, collection, me, idMap);
@@ -1958,7 +1994,7 @@ async function applyFieldFilters(docToProcess, model) {
1958
1994
  docToProcess[field.name],
1959
1995
  field
1960
1996
  );
1961
- const realFilter = Event.Trigger('OnDataFilter', "event", "system",filter, field, docToProcess );
1997
+ const realFilter = await Event.Trigger('OnDataFilter', "event", "system",filter, field, docToProcess );
1962
1998
  docToProcess[field.name] = realFilter || filter;
1963
1999
  }
1964
2000
  }
@@ -1968,7 +2004,7 @@ async function applyFieldFilters(docToProcess, model) {
1968
2004
  /**
1969
2005
  * Valide la structure et le contenu du document selon le modèle
1970
2006
  */
1971
- function validateModelData(doc, model, isPatch = false) {
2007
+ async function validateModelData(doc, model, isPatch = false) {
1972
2008
  if (!isPatch) {
1973
2009
  model.fields.forEach(field => {
1974
2010
  const value = doc[field.name];
@@ -1990,7 +2026,7 @@ function validateModelData(doc, model, isPatch = false) {
1990
2026
 
1991
2027
  const validator = dataTypes[fieldDef.type]?.validate;
1992
2028
  const valid = validator && validator(value, fieldDef);
1993
- const realValidation = Event.Trigger('OnDataValidate', "event", "system", value, fieldDef,doc );
2029
+ const realValidation = await Event.Trigger('OnDataValidate', "event", "system", value, fieldDef,doc );
1994
2030
  if (!(valid || realValidation)) {
1995
2031
  throw new Error(i18n.t('api.field.validationFailed', { field: fieldName, value }));
1996
2032
  }
@@ -2027,9 +2063,9 @@ async function insertAndResolveRelations(doc, model, collection, me, idMap) {
2027
2063
  docToProcess._id = new ObjectId(originalId);
2028
2064
  }
2029
2065
 
2030
- validateModelData(docToProcess, model);
2066
+ await validateModelData(docToProcess, model);
2031
2067
  await processRelations(docToProcess, model, collection, me, idMap);
2032
- validateModelData(docToProcess, model);
2068
+ await validateModelData(docToProcess, model);
2033
2069
  await applyFieldFilters(docToProcess, model);
2034
2070
  await checkUniqueFields(docToProcess, model, collection);
2035
2071
 
@@ -2271,9 +2307,9 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
2271
2307
  // 4. Validation adaptée pour patch ou edit (inchangé)
2272
2308
  if (!isPatch) {
2273
2309
  const dataToValidate = { ...existingDocs[0], ...updateData };
2274
- validateModelData(dataToValidate, model, false);
2310
+ await validateModelData(dataToValidate, model, false);
2275
2311
  } else {
2276
- validateModelData(updateData, model, true);
2312
+ await validateModelData(updateData, model, true);
2277
2313
  }
2278
2314
 
2279
2315
  // 5. Vérification des champs uniques (inchangé)
@@ -2374,6 +2410,17 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
2374
2410
  const bulkResult = await collection.bulkWrite(bulkOps);
2375
2411
  const modifiedCount = bulkResult.modifiedCount || 0;
2376
2412
 
2413
+ // Déclencher l'événement OnDataEdited avec les états avant/après
2414
+ if (modifiedCount > 0) {
2415
+ const updatedDocs = await collection.find({ _id: { $in: ids } }).toArray();
2416
+ await Event.Trigger("OnDataEdited", "event", "system", engine, {
2417
+ modelName,
2418
+ user,
2419
+ before: existingDocs, // Documents avant la modification
2420
+ after: updatedDocs // Documents après la modification
2421
+ });
2422
+ }
2423
+
2377
2424
  // 11. Tâches post-mise à jour (schedules, workflows) (inchangé)
2378
2425
  if (["workflowTrigger", "alert"].includes(modelName)) {
2379
2426
  await handleScheduledJobs(modelName, existingDocs, collection, finalDataForSet);
@@ -2434,7 +2481,7 @@ async function handleScheduledJobs(modelName, existingDocs, collection, updateDa
2434
2481
  schedule.scheduleJob(jobId, updatedDoc.cronExpression, async () => {
2435
2482
  logger.info(`[Scheduled Job] Cron triggered for job ${jobId}`);
2436
2483
  await runScheduledJobWithDbLock(jobId, async () => {
2437
- sendSseToUser(updatedDoc._user, {
2484
+ await sendSseToUser(updatedDoc._user, {
2438
2485
  type: 'cron_alert',
2439
2486
  triggerId: updatedDoc._id.toString(),
2440
2487
  triggerName: updatedDoc.name,
@@ -2657,8 +2704,8 @@ export const deleteData = async (modelName, filter, user ={}, triggerWorkflow, w
2657
2704
  }
2658
2705
 
2659
2706
  const res = { success: true, deletedCount }
2660
- const plugin = Event.Trigger("OnDataDeleted", "event", "system", engine, {model:modelName, filter});
2661
- Event.Trigger("OnDataDeleted", "event", "user", {model:modelName, filter});
2707
+ const plugin = await Event.Trigger("OnDataDeleted", "event", "system", engine, {model:modelName, filter});
2708
+ await Event.Trigger("OnDataDeleted", "event", "user", {model:modelName, filter});
2662
2709
  return plugin || res;
2663
2710
 
2664
2711
  } catch (error) {
@@ -3166,8 +3213,8 @@ export const searchData = async (query, user) => {
3166
3213
  data = await handleFields(modelElement, data, user);
3167
3214
 
3168
3215
  const res = {data, count: count[0]?.count || 0};
3169
- const plugin = Event.Trigger("OnDataSearched", "event", "system", engine, {data, count: count[0]?.count});
3170
- Event.Trigger("OnDataSearched", "event", "user", plugin || {data, count: count[0]?.count});
3216
+ const plugin = await Event.Trigger("OnDataSearched", "event", "system", engine, {data, count: count[0]?.count});
3217
+ await Event.Trigger("OnDataSearched", "event", "user", plugin || {data, count: count[0]?.count});
3171
3218
  return plugin || res;
3172
3219
  }
3173
3220
 
@@ -3244,7 +3291,7 @@ export const importData = async(options, files, user) => {
3244
3291
  });
3245
3292
  }
3246
3293
 
3247
- validateModelStructure(modelData);
3294
+ await validateModelStructure(modelData);
3248
3295
  await modelsCollection.insertOne(modelData);
3249
3296
  logger.info(`[Model Import] Successfully imported model '${modelName}' for user ${user.username}.`);
3250
3297
  } catch (e) {
@@ -3326,7 +3373,7 @@ export const importData = async(options, files, user) => {
3326
3373
  if (insertedIdsArray && insertedIdsArray.length > 0) {
3327
3374
  importJobs[importJobId].processedRecords += insertedIdsArray.length;
3328
3375
  logger.debug(`[Import Job ${importJobId}] Processed chunk for '${modelName}': ${insertedIdsArray.length} records. Total processed: ${importJobs[importJobId].processedRecords}`);
3329
- sendSseToUser(user.username, {
3376
+ await sendSseToUser(user.username, {
3330
3377
  type: 'import_progress',
3331
3378
  job: importJobs[importJobId]
3332
3379
  });
@@ -3430,7 +3477,7 @@ export const importData = async(options, files, user) => {
3430
3477
  const insertedIdsArray = await pushDataUnsecure(chunk, modelNameForImport, user, {});
3431
3478
  if (insertedIdsArray && insertedIdsArray.length > 0) {
3432
3479
  importJobs[importJobId].processedRecords += insertedIdsArray.length;
3433
- sendSseToUser(user.username, {
3480
+ await sendSseToUser(user.username, {
3434
3481
  type: 'import_progress',
3435
3482
  job: importJobs[importJobId]
3436
3483
  });
@@ -3524,7 +3571,7 @@ export const importData = async(options, files, user) => {
3524
3571
  const insertedIdsArray = await pushDataUnsecure(chunk, modelNameForImport, user, {});
3525
3572
  if (insertedIdsArray && insertedIdsArray.length > 0) {
3526
3573
  importJobs[importJobId].processedRecords += insertedIdsArray.length;
3527
- sendSseToUser(user.username, {
3574
+ await sendSseToUser(user.username, {
3528
3575
  type: 'import_progress',
3529
3576
  job: importJobs[importJobId]
3530
3577
  });
@@ -3569,7 +3616,7 @@ export const importData = async(options, files, user) => {
3569
3616
  } else {
3570
3617
  importJobs[importJobId].status = 'completed';
3571
3618
  }
3572
- sendSseToUser(user.username, {
3619
+ await sendSseToUser(user.username, {
3573
3620
  type: 'import_progress',
3574
3621
  job: importJobs[importJobId]
3575
3622
  });
@@ -3705,8 +3752,8 @@ export const exportData= async (options, user) =>{
3705
3752
  }
3706
3753
 
3707
3754
  const res = { success: true, data: exportResults, models: modelsToExport };
3708
- const plugin = Event.Trigger("OnDataExported", "event", "system", engine, exportResults, modelsToExport);
3709
- Event.Trigger("OnDataExported", "event", "user", plugin?.exportResults || exportResults, plugin?.modelsToExport || modelsToExport);
3755
+ const plugin = await Event.Trigger("OnDataExported", "event", "system", engine, exportResults, modelsToExport);
3756
+ await Event.Trigger("OnDataExported", "event", "user", plugin?.exportResults || exportResults, plugin?.modelsToExport || modelsToExport);
3710
3757
  return plugin || res;
3711
3758
  }
3712
3759
 
@@ -4058,7 +4105,7 @@ export const loadFromDump = async (user, options = {}) => {
4058
4105
  modelsCache.flushAll();
4059
4106
 
4060
4107
  logger.info(`[${action}] Restore successful for user ${user.username}.`);
4061
- Event.Trigger("OnDataRestored", "event", "system");
4108
+ await Event.Trigger("OnDataRestored", "event", "system");
4062
4109
 
4063
4110
  } finally {
4064
4111
  // --- GUARANTEED CLEANUP ---
@@ -4242,68 +4289,94 @@ async function manageBackupRotation(user, backupFrequency, s3Config = null) { //
4242
4289
  }
4243
4290
 
4244
4291
  /**
4245
- * Installe les modèles et les données d'un pack pour un utilisateur.
4246
- * Gère les modèles, les données, et les relations complexes (y compris les références futures)
4247
- * via un système en deux passes optimisé.
4292
+ * Installs pack models and data for a user or globally.
4293
+ * Can accept either a pack ID (to install from database) or a direct pack JSON object.
4248
4294
  *
4249
- * @param {object} logger - L'instance du logger.
4250
- * @param {string} packId - L'ID du pack à installer depuis la collection 'packs'.
4251
- * @param {object} user - L'objet utilisateur pour qui installer le pack.
4252
- * @param {string} lang - Le code de langue pour les données spécifiques.
4295
+ * @param {object} logger - Logger instance
4296
+ * @param {string|object} packIdentifier - Either pack ID (string) or pack JSON object
4297
+ * @param {object|null} user - User object (if installing for user) or null (for global install)
4298
+ * @param {string} [lang='en'] - Language code for localized data
4253
4299
  * @returns {Promise<{success: boolean, summary: object, errors: Array, modifiedCount: number}>}
4254
4300
  */
4255
- export async function installPack(packId, user, lang) {
4301
+ export async function installPack(packIdentifier, user = null, lang = 'en', isTemporary= false) {
4302
+ let pack;
4256
4303
  const packsCollection = getCollection('packs');
4257
- const pack = await packsCollection.findOne({ _id: new ObjectId(packId) });
4258
4304
 
4259
- if (!pack) {
4260
- throw new Error(`Pack with ID ${packId} not found.`);
4305
+ // Determine if we're working with an ID or direct pack object
4306
+ if (typeof packIdentifier === 'string') {
4307
+ // Existing behavior - fetch from database
4308
+ pack = await packsCollection.findOne({ _id: new ObjectId(packIdentifier) });
4309
+ if (!pack) {
4310
+ throw new Error(`Pack with ID ${packIdentifier} not found.`);
4311
+ }
4312
+ } else if (typeof packIdentifier === 'object' && packIdentifier !== null) {
4313
+ // New behavior - use provided pack object directly
4314
+ pack = packIdentifier;
4315
+
4316
+ // Validate basic pack structure
4317
+ if (!pack.name || (!pack.models && !pack.data)) {
4318
+ throw new Error('Invalid pack structure - must contain at least name and models or data');
4319
+ }
4320
+ } else {
4321
+ throw new Error('Invalid pack identifier - must be either pack ID string or pack object');
4261
4322
  }
4262
4323
 
4263
- logger.info(`--- Starting installation of pack '${pack.name}' for user '${user.username}' ---`);
4324
+ const username = user ? user.username : null;
4325
+ const logPrefix = username
4326
+ ? `Installing pack '${pack.name}' for user '${username}'`
4327
+ : `Installing pack '${pack.name}' globally`;
4328
+
4329
+ logger.info(`--- ${logPrefix} ---`);
4264
4330
 
4265
4331
  const summary = {
4266
4332
  models: { installed: [], skipped: [], failed: [] },
4267
4333
  datas: { inserted: 0, updated: 0, skipped: 0, failed: 0 }
4268
4334
  };
4269
4335
  const errors = [];
4270
- const collection = await getCollectionForUser(user);
4336
+ const collection = user ? await getCollectionForUser(user) : getCollection('data');
4271
4337
  const tempIdToNewIdMap = {};
4272
4338
  const linkCache = new Map();
4273
4339
 
4274
- // --- PHASE 1: Installation des Modèles ---
4340
+ // --- PHASE 1: MODEL INSTALLATION ---
4275
4341
  if (Array.isArray(pack.models)) {
4276
- const userModels = await modelsCollection.find({ _user: user.username }).toArray();
4277
- const userModelNames = userModels.map(m => m.name);
4342
+ // For user installs, check existing models
4343
+ const existingModels = user
4344
+ ? await modelsCollection.find({ _user: username }).toArray()
4345
+ : await modelsCollection.find({ _user: { $exists: false } }).toArray();
4346
+
4347
+ const existingModelNames = existingModels.map(m => m.name);
4278
4348
 
4279
4349
  for (const modelOrName of pack.models) {
4280
4350
  try {
4281
4351
  const modelName = typeof modelOrName === 'string' ? modelOrName : modelOrName?.name;
4282
4352
  if (!modelName) throw new Error('Model definition in pack is missing a name.');
4283
4353
 
4284
- if (userModelNames.includes(modelName)) {
4285
- logger.debug(`[Model Install] Skipping '${modelName}': already exists for user.`);
4354
+ if (existingModelNames.includes(modelName)) {
4355
+ logger.debug(`[Model Install] Skipping '${modelName}': already exists`);
4286
4356
  summary.models.skipped.push(modelName);
4287
4357
  continue;
4288
4358
  }
4289
4359
 
4290
- let modelToInstall;
4291
- if (typeof modelOrName === 'string') {
4292
- const sharedModel = await modelsCollection.findOne({ name: modelName, _user: { $exists: false } });
4293
- if (!sharedModel) throw new Error(`Shared model '${modelName}' not found.`);
4294
- const { _id, ...sharedModelData } = sharedModel;
4295
- modelToInstall = sharedModelData;
4296
- } else {
4297
- modelToInstall = { ...modelOrName };
4360
+ const modelToInstall = typeof modelOrName === 'string'
4361
+ ? await modelsCollection.findOne({ name: modelName, _user: { $exists: false } })
4362
+ : { ...modelOrName };
4363
+
4364
+ if (!modelToInstall) {
4365
+ throw new Error(`Model '${modelName}' not found in shared models`);
4298
4366
  }
4299
4367
 
4300
- modelToInstall._user = user.username;
4301
- delete modelToInstall._id;
4302
- modelToInstall.locked = false;
4303
- if (modelToInstall.fields) modelToInstall.fields.forEach(f => f.locked = false);
4368
+ // Prepare model for installation
4369
+ const preparedModel = { ...modelToInstall };
4370
+ if (user) preparedModel._user = username;
4371
+ delete preparedModel._id;
4372
+ preparedModel.locked = false;
4373
+
4374
+ if (preparedModel.fields) {
4375
+ preparedModel.fields.forEach(f => f.locked = false);
4376
+ }
4304
4377
 
4305
- validateModelStructure(modelToInstall);
4306
- await modelsCollection.insertOne(modelToInstall);
4378
+ await validateModelStructure(preparedModel);
4379
+ await modelsCollection.insertOne(preparedModel);
4307
4380
  summary.models.installed.push(modelName);
4308
4381
 
4309
4382
  } catch (e) {
@@ -4314,13 +4387,14 @@ export async function installPack(packId, user, lang) {
4314
4387
  }
4315
4388
  }
4316
4389
 
4317
- // --- PHASE 2: Installation des Données ---
4318
- const dataToInstall = { ...pack.data?.all, ...pack.data?.[user.lang || lang || 'en'] };
4319
- if (!dataToInstall || typeof dataToInstall !== 'object' || Object.keys(dataToInstall).length === 0) {
4390
+ // --- PHASE 2: DATA INSTALLATION ---
4391
+ const dataToInstall = { ...pack.data?.all, ...pack.data?.[lang] };
4392
+ if (!dataToInstall || Object.keys(dataToInstall).length === 0) {
4320
4393
  logger.warn(`Pack '${pack.name}' has no data to install.`);
4321
4394
  return { success: errors.length === 0, summary, errors, modifiedCount: 0 };
4322
4395
  }
4323
4396
 
4397
+ // Process link references (same as original)
4324
4398
  const linkQueue = [];
4325
4399
  for (const modelName in dataToInstall) {
4326
4400
  if (Array.isArray(dataToInstall[modelName])) {
@@ -4330,7 +4404,6 @@ export async function installPack(packId, user, lang) {
4330
4404
 
4331
4405
  for (const fieldName in docSource) {
4332
4406
  if (isPlainObject(docSource[fieldName]) && docSource[fieldName].$link) {
4333
- // CORRECTION 1: On ajoute le nom du modèle source à la file d'attente
4334
4407
  linkQueue.push({
4335
4408
  sourceTempId: tempId,
4336
4409
  sourceModelName: modelName,
@@ -4343,21 +4416,21 @@ export async function installPack(packId, user, lang) {
4343
4416
  }
4344
4417
  }
4345
4418
 
4346
- // --- PASSE 1: INSERTION PAR LOT ---
4347
- logger.info("[Pack Install] Starting Pass 1: Batch Insertion & ID Mapping.");
4419
+ // --- PASS 1: BATCH INSERTION ---
4420
+ logger.info("[Pack Install] Starting Pass 1: Batch Insertion & ID Mapping");
4348
4421
  for (const modelName in dataToInstall) {
4349
- if (!Object.prototype.hasOwnProperty.call(dataToInstall, modelName) || !Array.isArray(dataToInstall[modelName])) continue;
4422
+ if (!Array.isArray(dataToInstall[modelName])) continue;
4350
4423
 
4351
4424
  const documents = dataToInstall[modelName];
4352
4425
  if (documents.length === 0) continue;
4353
4426
 
4354
4427
  const docsToInsert = [];
4355
-
4356
4428
  const modelDefForHash = await getModel(modelName, user);
4357
4429
 
4358
4430
  for (const docSource of documents) {
4359
4431
  let docForInsert = { ...docSource };
4360
4432
 
4433
+ // Clear $link fields for first pass
4361
4434
  for (const key in docForInsert) {
4362
4435
  if (isPlainObject(docForInsert[key]) && docForInsert[key].$link) {
4363
4436
  docForInsert[key] = null;
@@ -4368,11 +4441,18 @@ export async function installPack(packId, user, lang) {
4368
4441
  delete docForInsert._id;
4369
4442
  delete docForInsert._temp_pack_id;
4370
4443
 
4371
- docForInsert._user = user.username;
4444
+ if (user) docForInsert._user = username;
4372
4445
  docForInsert._model = modelName;
4373
4446
  docForInsert._hash = getFieldValueHash(modelDefForHash, docForInsert);
4374
4447
 
4375
- const existingDoc = await collection.findOne({ _hash: docForInsert._hash, _user: user.username, _model: modelName }, { projection: { _id: 1 } });
4448
+ // Check for existing document
4449
+ const existingQuery = {
4450
+ _hash: docForInsert._hash,
4451
+ _model: modelName
4452
+ };
4453
+ if (user) existingQuery._user = username;
4454
+
4455
+ const existingDoc = await collection.findOne(existingQuery, { projection: { _id: 1 } });
4376
4456
  if (existingDoc) {
4377
4457
  tempIdToNewIdMap[tempId] = existingDoc._id;
4378
4458
  summary.datas.skipped++;
@@ -4398,7 +4478,6 @@ export async function installPack(packId, user, lang) {
4398
4478
  tempIdToNewIdMap[doc._temp_pack_id_for_mapping] = result.insertedIds[index];
4399
4479
  }
4400
4480
  });
4401
-
4402
4481
  } catch (e) {
4403
4482
  summary.datas.failed += docsToInsert.length;
4404
4483
  errors.push(`Error inserting batch for ${modelName}: ${e.message}`);
@@ -4407,83 +4486,79 @@ export async function installPack(packId, user, lang) {
4407
4486
  }
4408
4487
  }
4409
4488
 
4410
- // --- PASSE 2: LIAISON DES RÉFÉRENCES ---
4411
- logger.info(`[Pack Install] Starting Pass 2: Linking ${linkQueue.length} references.`);
4489
+ // --- PASS 2: REFERENCE LINKING ---
4490
+ logger.info(`[Pack Install] Starting Pass 2: Linking ${linkQueue.length} references`);
4412
4491
  for (const linkOp of linkQueue) {
4413
- // CORRECTION 2: On récupère le nom du modèle source
4414
4492
  const { sourceTempId, sourceModelName, fieldName, linkSelector } = linkOp;
4415
-
4416
4493
  const sourceId = tempIdToNewIdMap[sourceTempId];
4494
+
4417
4495
  if (!sourceId) {
4418
4496
  logger.warn(`[LINK FAILED] Could not find newly inserted document for temp ID ${sourceTempId}. Skipping link.`);
4419
4497
  continue;
4420
4498
  }
4421
4499
 
4422
- const cacheKey = JSON.stringify({ selector: linkSelector, user: user.username });
4423
- let targetIds = null; // Renommé en 'targetIds' car c'est toujours un tableau
4424
-
4425
- const targetModelName = linkSelector._model;
4426
- delete linkSelector['_model'];
4500
+ try {
4501
+ const targetModelName = linkSelector._model;
4502
+ delete linkSelector._model;
4427
4503
 
4428
- // CORRECTION 3: On récupère la définition du modèle SOURCE
4429
- const sourceModelDef = await getModel(sourceModelName, user);
4504
+ const sourceModelDef = await getModel(sourceModelName, user);
4505
+ const fieldDef = sourceModelDef.fields.find(f => f.name === fieldName);
4430
4506
 
4431
- try {
4432
- if (linkCache.has(cacheKey)) {
4433
- targetIds = linkCache.get(cacheKey);
4434
- logger.debug(`[LINK CACHE HIT] for ${cacheKey}`);
4435
- } else {
4436
- const finalSelector = { ...linkSelector };
4437
- delete finalSelector['_model']; // nécessaire
4438
- // CORRECTION 4: Appel corrigé à searchData
4439
- const { data: targetDocs } = await searchData({model: targetModelName, filter: finalSelector }, user);
4440
-
4441
- if (targetDocs && targetDocs.length > 0) {
4442
- targetIds = targetDocs.map(d => d._id); // Récupère un tableau d'ObjectIds
4443
- linkCache.set(cacheKey, targetIds);
4444
- }
4507
+ if (!fieldDef) {
4508
+ logger.warn(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}'`);
4509
+ errors.push(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}'`);
4510
+ summary.datas.failed++;
4511
+ continue;
4445
4512
  }
4446
4513
 
4447
- if (targetIds && targetIds.length > 0) {
4448
- // CORRECTION 5: On cherche le champ dans la définition du modèle SOURCE
4449
- const fieldDef = sourceModelDef.fields.find(f => f.name === fieldName);
4450
- if (!fieldDef) {
4451
- logger.warn(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}' for doc ${sourceId}.`);
4452
- errors.push(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}' for doc ${sourceId}.`);
4453
- summary.datas.failed++;
4454
- continue;
4455
- }
4456
-
4457
- // CORRECTION 6: On gère correctement les cas multiples et uniques
4458
- const valueToSet = fieldDef.multiple ? targetIds.map(id => id.toString()) : targetIds[0].toString();
4514
+ // Search for target documents
4515
+ const { data: targetDocs } = await searchData(
4516
+ { model: targetModelName, filter: linkSelector },
4517
+ user
4518
+ );
4459
4519
 
4460
- await collection.updateOne(
4461
- { _id: sourceId },
4462
- { $set: { [fieldName]: valueToSet } }
4463
- );
4464
- summary.datas.updated++;
4465
- } else {
4466
- const errorMsg = `[LINK FAILED] Could not find target document for linking: ${JSON.stringify(linkSelector)}`;
4520
+ if (!targetDocs || targetDocs.length === 0) {
4521
+ const errorMsg = `[LINK FAILED] No target found for ${JSON.stringify(linkSelector)}`;
4467
4522
  logger.warn(errorMsg);
4468
4523
  errors.push(errorMsg);
4469
4524
  summary.datas.failed++;
4525
+ continue;
4470
4526
  }
4527
+
4528
+ // Update source document with reference
4529
+ const valueToSet = fieldDef.multiple
4530
+ ? targetDocs.map(d => d._id.toString())
4531
+ : targetDocs[0]._id.toString();
4532
+
4533
+ await collection.updateOne(
4534
+ { _id: sourceId },
4535
+ { $set: { [fieldName]: valueToSet } }
4536
+ );
4537
+ summary.datas.updated++;
4538
+
4471
4539
  } catch (e) {
4472
- const errorMsg = `[LINK CRITICAL] Error during linking for doc ${sourceId}: ${e.message}`;
4540
+ const errorMsg = `[LINK CRITICAL] Error linking ${sourceModelName}.${fieldName}: ${e.message}`;
4473
4541
  logger.error(errorMsg, e.stack);
4474
4542
  errors.push(errorMsg);
4475
4543
  summary.datas.failed++;
4476
4544
  }
4477
4545
  }
4478
4546
 
4479
- Event.Trigger("OnPackInstalled", "event", "system", pack);
4547
+ // Trigger event only if pack came from database (original behavior)
4548
+ if (typeof packIdentifier === 'string') {
4549
+ await Event.Trigger("OnPackInstalled", "event", "system", pack);
4550
+ }
4480
4551
 
4481
4552
  const modifiedCount = summary.datas.inserted + summary.datas.updated;
4482
- logger.info(`--- Installation of pack '${pack.name}' finished. ---`);
4483
- return { success: errors.length === 0, summary, errors, modifiedCount };
4553
+ logger.info(`--- ${logPrefix} completed ---`);
4554
+ return {
4555
+ success: errors.length === 0,
4556
+ summary,
4557
+ errors,
4558
+ modifiedCount
4559
+ };
4484
4560
  }
4485
4561
 
4486
-
4487
4562
  export const installAllPacks = async () => {
4488
4563
  const packs = await getAllPacks();
4489
4564
  await packsCollection.deleteMany({ _user: { $exists : false }});
@@ -4528,14 +4603,7 @@ export async function handleDemoInitialization(req, res) {
4528
4603
  logger.info(`[Demo Init] Installing dynamically generated pack with models: [${models.join(', ')}].`);
4529
4604
 
4530
4605
  // Create and install pack
4531
- const packsCollection = getCollection('packs');
4532
- const tempPack = { ...packToInstall, _user: 'system_temp' };
4533
- const tempInsert = await packsCollection.insertOne(tempPack);
4534
- const packId = tempInsert.insertedId;
4535
-
4536
- const result = await installPack(packId, user, req.query.lang || 'en');
4537
-
4538
- await packsCollection.deleteOne({ _id: packId });
4606
+ const result = await installPack(packToInstall, user, req.query.lang || 'en');
4539
4607
 
4540
4608
  if (result.success || result.modifiedCount > 0) {
4541
4609
  logger.info(`[Demo Init] Pack installed successfully for user '${user.username}'.`);