data-primals-engine 1.6.3 → 1.7.0

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 (55) hide show
  1. package/README.md +52 -900
  2. package/client/index.js +3 -0
  3. package/client/package-lock.json +7563 -8811
  4. package/client/package.json +11 -1
  5. package/client/src/App.scss +29 -0
  6. package/client/src/AssistantChat.jsx +369 -362
  7. package/client/src/DataEditor.jsx +383 -383
  8. package/client/src/DataLayout.jsx +54 -20
  9. package/client/src/ModelList.jsx +280 -280
  10. package/client/src/ViewSwitcher.scss +0 -31
  11. package/client/src/_variables.scss +3 -0
  12. package/client/src/constants.js +81 -100
  13. package/client/src/contexts/CommandContext.jsx +274 -259
  14. package/client/vite.config.js +30 -30
  15. package/doc/AI-assistance.md +93 -0
  16. package/doc/Advanced-workflows.md +90 -0
  17. package/doc/Event-system.md +79 -0
  18. package/doc/Packs-gallery.md +73 -0
  19. package/doc/automation-workflows.md +102 -0
  20. package/doc/core-concepts.md +33 -0
  21. package/doc/custom-api-endpoints.md +40 -0
  22. package/doc/dashboards-kpis-charts.md +49 -0
  23. package/doc/data-management.md +120 -0
  24. package/doc/data-models.md +75 -0
  25. package/doc/index.md +14 -0
  26. package/doc/roles-permissions.md +43 -0
  27. package/doc/users.md +30 -0
  28. package/package.json +20 -10
  29. package/src/client.js +6 -4
  30. package/src/constants.js +1 -1
  31. package/src/core.js +31 -12
  32. package/src/defaultModels.js +1 -1
  33. package/src/engine.js +342 -335
  34. package/src/filter.js +72 -173
  35. package/src/migrate.js +1 -1
  36. package/src/modules/assistant/assistant.js +30 -20
  37. package/src/modules/data/data.backup.js +4 -4
  38. package/src/modules/data/data.js +8 -7
  39. package/src/modules/data/data.operations.js +186 -133
  40. package/src/modules/data/data.relations.js +3 -2
  41. package/src/modules/data/data.scheduling.js +1 -1
  42. package/src/modules/mongodb.js +17 -8
  43. package/src/modules/swagger.js +25 -5
  44. package/src/modules/user.js +108 -79
  45. package/src/modules/workflow.js +138 -3
  46. package/src/packs.js +5697 -5697
  47. package/src/profiles.js +19 -0
  48. package/swagger-en.yml +3391 -1550
  49. package/swagger-fr.yml +3385 -2896
  50. package/test/assistant.test.js +2 -2
  51. package/test/core.test.js +341 -0
  52. package/test/data.backup.integration.test.js +3 -1
  53. package/test/data.history.integration.test.js +0 -1
  54. package/test/data.integration.test.js +137 -2
  55. package/test/user.test.js +33 -29
@@ -24,7 +24,7 @@ import {
24
24
  optionsSanitizer,
25
25
  searchRequestTimeout
26
26
  } from "../../constants.js";
27
- import {anonymizeText, getFieldValueHash, getUserId, isDemoUser, isLocalUser} from "../../data.js";
27
+ import {anonymizeText, encryptValue, getFieldValueHash, getUserId, isDemoUser, isLocalUser} from "../../data.js";
28
28
  import sanitizeHtml from "sanitize-html";
29
29
  import {importJobs, modelsCache, runCryptoWorkerTask, runImportExportWorker} from "./data.core.js";
30
30
  import {
@@ -37,7 +37,7 @@ import {
37
37
  } from "../mongodb.js";
38
38
  import i18n from "../../i18n.js";
39
39
  import tinycolor from 'tinycolor2';
40
- import {Config} from "../../config.js";
40
+ import { Config } from "../../config.js";
41
41
  import {calculateTotalUserStorageUsage, hasPermission} from "../user.js";
42
42
  import {BSON, ObjectId} from "mongodb";
43
43
  import {runScheduledJobWithDbLock, triggerWorkflows} from "../workflow.js";
@@ -45,7 +45,7 @@ import schedule from "node-schedule";
45
45
  import {sendSseToUser} from "./data.routes.js";
46
46
  import {applyCronMask, handleScheduledJobs, runStatefulAlertJob} from "./data.scheduling.js";
47
47
  import {Event} from "../../events.js";
48
- import {getAllPacks} from "../../packs.js";
48
+ import { getAllPacks } from "../../packs.js";
49
49
  import {validateModelData, validateModelStructure} from "./data.validation.js";
50
50
  import {addFile, removeFile} from "../file.js";
51
51
  import NodeCache from "node-cache";
@@ -61,7 +61,8 @@ import {
61
61
  processDocuments
62
62
  } from "./data.relations.js";
63
63
  import crypto from 'crypto';
64
- import cronstrue from 'cronstrue/i18n.js';
64
+ import cronstrue from 'cronstrue/i18n.js'
65
+ import { isConditionMet } from '../../filter.js';
65
66
  import util from "node:util";
66
67
 
67
68
  const delay = ms => new Promise(res => setTimeout(res, ms));
@@ -70,7 +71,7 @@ const IMPORT_CHUNK_DELAY_MS = 1000; // Délai en millisecondes entre le traiteme
70
71
 
71
72
  export const dataTypes = {
72
73
  object: {
73
- validate: (value, field) => {
74
+ validate: (value, field) => {
74
75
  return value === null || isPlainObject(value);
75
76
  }
76
77
  },
@@ -111,7 +112,13 @@ export const dataTypes = {
111
112
  const ml = Math.min(Math.max(field.maxlength, 0), m);
112
113
  return value === null || typeof value === 'string' && (!ml || value.length <= ml)
113
114
  },
114
- anonymize: anonymizeText
115
+ anonymize: anonymizeText,
116
+ filter: async (value, field) => {
117
+ if (field.encrypted && value) {
118
+ return encryptValue(value);
119
+ }
120
+ return value;
121
+ }
115
122
  },
116
123
  code: {
117
124
  validate: (value, field) => {
@@ -141,8 +148,12 @@ export const dataTypes = {
141
148
  const ml = Math.min(Math.max(field.maxlength, 0), m);
142
149
  return value === null || typeof value === 'string' && (!ml || value.length <= ml)
143
150
  },
144
- filter: async (value) => {
145
- return sanitizeHtml(value, optionsSanitizer);
151
+ filter: async (value, field) => {
152
+ const sanitized = sanitizeHtml(value, optionsSanitizer);
153
+ if (field.encrypted && sanitized) {
154
+ return encryptValue(sanitized);
155
+ }
156
+ return sanitized;
146
157
  },
147
158
  anonymize: anonymizeText
148
159
  },
@@ -151,7 +162,7 @@ export const dataTypes = {
151
162
  if (value === null)
152
163
  return true;
153
164
  const m = Config.Get('maxStringLength', maxStringLength);
154
- const ml = Math.min(Math.max(field.maxlength, 0), maxStringLength);
165
+ const ml = Math.min(Math.max(field.maxlength, 0), m);
155
166
  // La valeur peut être une chaîne de caractères...
156
167
  if (typeof value === 'string') {
157
168
  return !ml || value.length <= ml;
@@ -693,14 +704,27 @@ export const getModel = async (modelName, user) => {
693
704
  export const getModels = async () => {
694
705
  return await getCollection('models')?.find({'$or': [{_user: {$exists: false}}]}).toArray() || [];
695
706
  }
696
- export const insertData = async (modelName, data, files, user, triggerWorkflow = true, waitForWorkflow = true) => {
707
+ export const insertData = async (modelName, data, files, user, triggerWorkflow = true, waitForWorkflow = true, checkPermissions = true) => {
697
708
 
698
- // --- Vérification des permissions (inchangée) ---
699
- if (!(isDemoUser(user) && Config.Get("useDemoAccounts")) && isLocalUser(user) && (
700
- !await hasPermission(["API_ADMIN", "API_ADD_DATA", "API_ADD_DATA_" + modelName], user) ||
701
- await hasPermission(["API_ADD_DATA_NOT_" + modelName], user))) {
702
- // Renvoyer une structure d'erreur cohérente
703
- return {success: false, error: i18n.t('api.permission.addData'), statusCode: 403};
709
+ if (checkPermissions) {
710
+ // --- Vérification des permissions ---
711
+ const permissionFilter = await hasPermission(["API_ADMIN", "API_ADD_DATA", "API_ADD_DATA_" + modelName], user);
712
+ const hasDeniedPermission = await hasPermission(["API_ADD_DATA_NOT_" + modelName], user);
713
+
714
+ if (!(isDemoUser(user) && Config.Get("useDemoAccounts")) && isLocalUser(user) && (!permissionFilter || hasDeniedPermission)) {
715
+ return { success: false, error: i18n.t('api.permission.addData', "You do not have permission to add this data."), statusCode: 403 };
716
+ }
717
+
718
+ // Si un filtre de permission existe, valider les données entrantes
719
+ if (typeof permissionFilter === 'object' && Object.keys(permissionFilter).length > 0) {
720
+ const dataArray = Array.isArray(data) ? data : [data];
721
+ const model = await getModel(modelName, user);
722
+ for (const doc of dataArray) {
723
+ if (!isConditionMet(model, permissionFilter, doc, [], user)) {
724
+ return { success: false, error: i18n.t('api.permission.addData.filterViolation', "The data you are trying to add does not meet the required permission criteria."), statusCode: 403 };
725
+ }
726
+ }
727
+ }
704
728
  }
705
729
 
706
730
  const collection = await getCollectionForUser(user);
@@ -836,7 +860,7 @@ export const insertData = async (modelName, data, files, user, triggerWorkflow =
836
860
  return {success: true, data: firstInsertedDoc, insertedIds: insertedIds.map(id => id.toString())};
837
861
 
838
862
  } catch (error) { // Attrape les erreurs de permission ou de pushDataUnsecure
839
- logger.error(`[insertData] Main error during insertion process for model ${modelName}: ${error.message}`, error.stack);
863
+ logger?.error(`[insertData] Main error during insertion process for model ${modelName}: ${error.message}`, error.stack);
840
864
  // Renvoyer une structure d'erreur cohérente
841
865
  return {
842
866
  success: false,
@@ -1084,27 +1108,40 @@ export const editData = async (modelName, filter, data, files, user, triggerWork
1084
1108
  const internalEditOrPatchData = async (modelName, filter, data, files, user, isPatch, triggerWorkflow = true, waitForWorkflow = false) => {
1085
1109
  try {
1086
1110
  // 1. Vérification des permissions
1087
- if (user.username !== 'demo' && isLocalUser(user) && (
1088
- !await hasPermission(["API_ADMIN", "API_EDIT_DATA", "API_EDIT_DATA_" + modelName], user) ||
1089
- await hasPermission(["API_EDIT_DATA_NOT_" + modelName], user))) {
1090
- throw new Error(i18n.t("api.permission.editData"));
1111
+ const permissionResult = await hasPermission(["API_ADMIN", "API_EDIT_DATA", "API_EDIT_DATA_" + modelName], user);
1112
+ const hasDeniedPermission = await hasPermission(["API_EDIT_DATA_NOT_" + modelName], user);
1113
+
1114
+ if (user.username !== 'demo' && isLocalUser(user) && (!permissionResult || hasDeniedPermission)) {
1115
+ throw new Error(i18n.t("api.permission.editData", "You do not have permission to edit this data."));
1091
1116
  }
1092
1117
 
1118
+
1093
1119
  const collection = await getCollectionForUser(user);
1094
- const model = await modelsCollection.findOne({name: modelName, _user: user.username});
1120
+ const model = await modelsCollection.findOne({$and : [{name: modelName}, {$or: [{_user: user._user}, {_user:user.username}]}]});
1095
1121
  if (!model) {
1096
1122
  throw new Error(i18n.t("api.model.notFound", {model: modelName}));
1097
1123
  }
1098
1124
 
1099
- // 2. Récupération des documents existants et de leur hash original
1100
- const existingDocs = (await searchData({model: modelName, filter}, user))?.data;
1125
+ // 2. Combinaison du filtre de la requête et du filtre de permission
1126
+ let combinedFilter = filter;
1127
+ if (permissionResult && typeof permissionResult === 'object' && Object.keys(permissionResult).length > 0) {
1128
+ combinedFilter = {
1129
+ $and: [filter, permissionResult]
1130
+ };
1131
+ }
1132
+
1133
+ // 3. Récupération des documents à modifier en utilisant le filtre combiné
1134
+ const docsToEditQuery = await searchData({model: modelName, filter: combinedFilter}, user);
1135
+ let existingDocs = docsToEditQuery.data;
1136
+
1101
1137
  if (!existingDocs || existingDocs.length === 0) {
1102
- return {success: false, error: i18n.t("api.data.notFound")};
1138
+ throw new Error(i18n.t("api.data.notFound"));
1103
1139
  }
1140
+
1104
1141
  const ids = existingDocs.map(d => new ObjectId(d._id));
1105
1142
  const originalHash = existingDocs[0]._hash; // Sauvegarde du hash avant modification
1106
1143
 
1107
- // 3. Préparation des données de mise à jour en séparant les opérateurs
1144
+ // 4. Préparation des données de mise à jour en séparant les opérateurs
1108
1145
  const updateSetData = {};
1109
1146
  const updatePushData = {};
1110
1147
 
@@ -1164,15 +1201,10 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1164
1201
  }
1165
1202
  }
1166
1203
 
1167
- // 4. Validation adaptée pour patch ou edit
1168
- if (!isPatch) {
1169
- const dataToValidate = {...existingDocs[0], ...updateSetData};
1170
- await validateModelData(dataToValidate, model, false);
1171
- } else {
1172
- await validateModelData(updateSetData, model, true);
1173
- }
1204
+ // 5. Validation adaptée pour patch ou edit
1205
+ await validateModelData(isPatch ? updateSetData : { ...existingDocs[0], ...updateSetData }, model, isPatch);
1174
1206
 
1175
- // 4.1 Validation pour les opérations $push
1207
+ // 5.1 Validation pour les opérations $push
1176
1208
  for (const fieldName in updatePushData) {
1177
1209
  const field = model.fields.find(f => f.name === fieldName);
1178
1210
  if (!field) throw new Error(`Le champ '${fieldName}' pour l'opération $push n'existe pas dans le modèle '${model.name}'.`);
@@ -1197,7 +1229,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1197
1229
  }
1198
1230
 
1199
1231
 
1200
- // 5. Vérification des champs uniques (s'applique uniquement aux opérations $set)
1232
+ // 6. Vérification des champs uniques (s'applique uniquement aux opérations $set)
1201
1233
  const uniqueFields = model.fields.filter(f => f.unique);
1202
1234
  for (const field of uniqueFields) {
1203
1235
  if (updateData[field.name] !== undefined) {
@@ -1216,7 +1248,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1216
1248
  }
1217
1249
  }
1218
1250
 
1219
- // 6. Traitement des relations (pour $set)
1251
+ // 7. Traitement des relations (pour $set)
1220
1252
  const relationFields = model.fields.filter(f => f.type === 'relation');
1221
1253
  for (const field of relationFields) {
1222
1254
  if (updateData[field.name] !== undefined) {
@@ -1240,7 +1272,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1240
1272
  }
1241
1273
  }
1242
1274
 
1243
- // 7. Application des filtres de champ (pour $set)
1275
+ // 8. Application des filtres de champ (pour $set)
1244
1276
  for (const field of model.fields) {
1245
1277
  // On saute les champs 'file' car ils ont déjà été traités
1246
1278
  if (field.type !== 'file' && field.itemsType !== 'file' && updateData[field.name] !== undefined && dataTypes[field.type]?.filter) {
@@ -1286,7 +1318,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1286
1318
  }
1287
1319
  }
1288
1320
 
1289
- // 8. Vérification des limites de stockage avant la mise à jour
1321
+ // 9. Vérification des limites de stockage avant la mise à jour
1290
1322
  const originalDocsSize = calculateDataSize(existingDocs);
1291
1323
  const finalStateForSize = existingDocs.map(doc => {
1292
1324
  const updatedDoc = { ...doc, ...updateSetData };
@@ -1323,7 +1355,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1323
1355
  }
1324
1356
  }
1325
1357
 
1326
- // 8. Calcul du nouveau hash en simulant l'état final
1358
+ // 10. Calcul du nouveau hash en simulant l'état final
1327
1359
  const finalStateForHash = { ...existingDocs[0], ...updateSetData };
1328
1360
  for (const fieldName in updatePushData) {
1329
1361
  let itemsToAdd;
@@ -1358,7 +1390,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1358
1390
  }
1359
1391
  }
1360
1392
 
1361
- // 9. *** CORRECTION LOGIQUE ***
1393
+ // 11. *** CORRECTION LOGIQUE ***
1362
1394
  // On ne vérifie l'unicité que si le hash a réellement changé.
1363
1395
  if (newHash !== originalHash) {
1364
1396
  const hashCheck = await checkHash(user, model, newHash, existingDocs[0]._id.toString());
@@ -1368,7 +1400,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1368
1400
  }
1369
1401
  }
1370
1402
 
1371
- // 10. Exécution de la mise à jour.
1403
+ // 12. Exécution de la mise à jour.
1372
1404
  // Si une opération $push est présente, nous devons utiliser une pipeline d'agrégation
1373
1405
  // pour gérer de manière robuste le cas où le champ cible est `null` ou n'existe pas.
1374
1406
  let updateCommand;
@@ -1430,7 +1462,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1430
1462
  });
1431
1463
  }
1432
1464
 
1433
- // 11. Tâches post-mise à jour (schedules, workflows) (inchangé)
1465
+ // 13. Tâches post-mise à jour (schedules, workflows) (inchangé)
1434
1466
  if (["workflowTrigger", "alert"].includes(modelName)) {
1435
1467
  await handleScheduledJobs(modelName, existingDocs, collection, finalUpdateOperation.$set || {});
1436
1468
  }
@@ -1452,47 +1484,72 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1452
1484
  };
1453
1485
 
1454
1486
  } catch (error) {
1455
- logger.error("Erreur lors de la mise à jour de la ressource :", error);
1487
+ logger?.error("Erreur lors de la mise à jour de la ressource :", error);
1456
1488
  return {success: false, error: error.message};
1457
1489
  }
1458
1490
  };
1459
1491
  export const deleteData = async (modelName, filter, user = {}, triggerWorkflow, waitForWorkflow = false) => {
1460
1492
 
1461
1493
  try {
1462
- const collection = await getCollectionForUser(user);
1494
+ const permissionFilter = await hasPermission(["API_ADMIN", "API_DELETE_DATA", "API_DELETE_DATA_" + modelName], user);
1495
+ const hasDeniedPermission = await hasPermission(["API_DELETE_DATA_NOT_" + modelName], user);
1463
1496
 
1464
- // --- Début de la logique de suppression ---
1497
+ if (user?.username !== 'demo' && isLocalUser(user) && (!permissionFilter || hasDeniedPermission)) {
1498
+ // Si l'utilisateur n'a pas la permission de base, on arrête tout de suite.
1499
+ return { success: false, error: i18n.t("api.permission.deleteData", "You do not have permission to delete this data."), deletedCount: 0 };
1500
+ }
1465
1501
 
1466
- // 1. Construire le filtre de base pour trouver les documents à supprimer
1467
- let findFilter = [];
1468
- if (user)
1469
- findFilter.push({
1470
- '$eq': ["$_user", user.username]
1471
- });
1502
+ const collection = await getCollectionForUser(user);
1472
1503
 
1473
- // Ajouter le filtre par IDs si fourni
1474
- if (Array.isArray(filter) && filter.length > 0) {
1475
- findFilter.push({"$in": ["$_id", filter.map(m => new ObjectId(m))]});
1476
- }
1504
+ // 1. Construire le filtre de recherche.
1505
+ // Nous allons utiliser un seul objet $match avec un $and global pour combiner toutes les conditions.
1506
+ const matchConditions = [];
1477
1507
 
1478
- // Ajouter le filtre par nom de modèle si fourni (utile si 'filter' est utilisé seul)
1479
- if (modelName)
1480
- findFilter.push({
1481
- '$eq': ["$_model", modelName]
1482
- });
1508
+ // Conditions de base (modèle et utilisateur)
1509
+ if (user) {
1510
+ matchConditions.push({ $expr: { '$eq': ["$_user", user.username] } });
1511
+ }
1512
+ if (modelName) {
1513
+ matchConditions.push({ _model: modelName });
1514
+ }
1483
1515
 
1484
- // Ajouter le filtre supplémentaire si fourni
1485
- if (filter && typeof filter === 'object' && Object.keys(filter).length > 0) {
1486
- // Fusionner prudemment le filtre supplémentaire
1487
- // Attention: Si 'filter' contient des clés comme _id ou _user,
1488
- // cela pourrait entrer en conflit. Une fusion plus robuste pourrait re nécessaire.
1489
- findFilter.push(filter);
1490
- } else {
1516
+ // Filtre de la requête (par ID ou autre)
1517
+ if (Array.isArray(filter) && filter.length > 0 && isObjectId(filter[0])) {
1518
+ // Filtre par tableau d'IDs
1519
+ matchConditions.push({ _id: { "$in": filter.map(m => new ObjectId(m)) } });
1520
+ } else if (filter && typeof filter === 'object' && Object.keys(filter).length > 0) {
1521
+ const firstKey = Object.keys(filter)[0];
1522
+ // Si le filtre est une expression d'agrégation (commence par $), on l'encapsule dans $expr
1523
+ if (firstKey.startsWith('$')) {
1524
+ matchConditions.push({ $expr: filter });
1525
+ } else {
1526
+ // Sinon, on le traite comme un filtre de document standard
1527
+ const processedFilter = {};
1528
+ for (const key in filter) {
1529
+ if (key === '_id' && !isObjectId(filter[key])) {
1530
+ // S'assurer que la conversion ne se fait que si la chaîne est un ID valide
1531
+ if (ObjectId.isValid(filter[key])) {
1532
+ processedFilter[key] = new ObjectId(filter[key]);
1533
+ } else {
1534
+ processedFilter[key] = filter[key]; // Garder la valeur originale si non valide
1535
+ }
1536
+ } else {
1537
+ processedFilter[key] = filter[key];
1538
+ }
1539
+ }
1540
+ matchConditions.push(processedFilter);
1541
+ }
1542
+ }
1491
1543
 
1544
+ // Filtre de permission
1545
+ if (permissionFilter && typeof permissionFilter === 'object') {
1546
+ matchConditions.push(permissionFilter);
1492
1547
  }
1493
1548
 
1549
+ const findFilter = matchConditions.length > 0 ? { $and: matchConditions } : {};
1550
+
1494
1551
  // 2. Récupérer les documents à supprimer pour vérifier leur type et annuler les schedules
1495
- const documentsToDelete = await collection.aggregate([{$match: {$expr: {"$and": findFilter}}}]).toArray();
1552
+ const documentsToDelete = await collection.aggregate([{ $match: findFilter }]).toArray();
1496
1553
 
1497
1554
  if (documentsToDelete.length === 0) {
1498
1555
  logger.info(`[deleteData] No documents found matching the criteria for user ${user?.username}.`);
@@ -1531,15 +1588,6 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
1531
1588
  await Promise.allSettled(deletePromises);
1532
1589
  }
1533
1590
 
1534
- // Vérification des permissions (pour chaque document trouvé)
1535
- if (user?.username !== 'demo' && isLocalUser(user) && (
1536
- !await hasPermission(["API_ADMIN", "API_DELETE_DATA", "API_DELETE_DATA_" + docToDelete._model], user) ||
1537
- await hasPermission(["API_DELETE_DATA_NOT_" + docToDelete._model], user))) {
1538
- // Si l'utilisateur n'a pas la permission pour CE document spécifique, on l'ignore
1539
- logger.warn(`[deleteData] User ${user.username} lacks permission to delete document ${docToDelete._id} of model ${docToDelete._model}. Skipping.`);
1540
- continue; // Passe au document suivant
1541
- }
1542
-
1543
1591
  // *** Ajout de l'annulation du schedule pour workflowTrigger ***
1544
1592
  if (docToDelete._model === 'workflowTrigger') {
1545
1593
  const jobId = `workflowTrigger_${docToDelete._id}`;
@@ -1678,9 +1726,10 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
1678
1726
  } else {
1679
1727
  logger.info(`[deleteData] No documents to delete for user ${user?.username} after permission checks or matching criteria.`);
1680
1728
  }
1681
-
1682
1729
  const res = { success: true, deletedCount };
1683
1730
 
1731
+ if (!modelNameToInvalidate)
1732
+ return res;
1684
1733
  // --- CORRECTION ---
1685
1734
  // The event payload must match what the history listener expects: { modelName, user, before }.
1686
1735
  // 'documentsToDelete' contains the full documents before they were deleted.
@@ -1807,7 +1856,6 @@ export const searchData = async (query, user) => {
1807
1856
  // Vérifier si les données sont en cache
1808
1857
  const cachedData = searchCache.get(cacheKey);
1809
1858
  if (cachedData) {
1810
- console.log('Cache hit for key:', cacheKey);
1811
1859
  return cachedData;
1812
1860
  }
1813
1861
 
@@ -3052,7 +3100,6 @@ export const exportData = async (options, user) => {
3052
3100
  * Installs pack models and data for a user or globally.
3053
3101
  * Can accept either a pack ID (to install from database) or a direct pack JSON object.
3054
3102
  *
3055
- * @param {object} logger - Logger instance
3056
3103
  * @param {string|object} packIdentifier - Either pack ID (string) or pack JSON object
3057
3104
  * @param {object|null} user - User object (if installing for user) or null (for global install)
3058
3105
  * @param {string} [lang='en'] - Language code for localized data
@@ -3061,7 +3108,6 @@ export const exportData = async (options, user) => {
3061
3108
  export async function installPack(packIdentifier, user = null, lang = 'en', options = {}) {
3062
3109
  let pack;
3063
3110
  const packsCollection = getCollection('packs');
3064
-
3065
3111
  // Determine if we're working with an ID or direct pack object
3066
3112
  if (typeof packIdentifier === 'string') {
3067
3113
  let p;
@@ -3101,12 +3147,19 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3101
3147
  throw new Error('Invalid pack identifier - must be either pack ID string or pack object');
3102
3148
  }
3103
3149
 
3150
+ // Vérifier si le pack est déjà installé pour cet utilisateur
3151
+ const existingPack = await packsCollection.findOne({ name: pack.name, _user: user?.username });
3152
+ if (existingPack) {
3153
+ logger?.warn(`Pack '${pack.name}' is already installed for user '${user?.username}'. Skipping installation.`);
3154
+ return { success: true, summary: {}, errors: [], modifiedCount: 0 };
3155
+ }
3156
+
3104
3157
  const username = user ? user.username : null;
3105
3158
  const logPrefix = username
3106
3159
  ? `Installing pack '${pack.name}' for user '${username}'`
3107
3160
  : `Installing pack '${pack.name}' globally`;
3108
3161
 
3109
- logger.info(`--- ${logPrefix} ---`);
3162
+ logger?.info(`--- ${logPrefix} ---`);
3110
3163
 
3111
3164
  const summary = {
3112
3165
  models: {installed: [], skipped: [], failed: []},
@@ -3121,9 +3174,8 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3121
3174
  if (Array.isArray(pack.models)) {
3122
3175
  // For user installs, check existing models
3123
3176
  const existingModels = user
3124
- ? await modelsCollection.find({_user: username}).toArray()
3177
+ ? await modelsCollection.find({_user: user.username}).toArray()
3125
3178
  : await modelsCollection.find({_user: {$exists: false}}).toArray();
3126
-
3127
3179
  const existingModelNames = existingModels.map(m => m.name);
3128
3180
 
3129
3181
  for (let i = 0; i < pack.models.length; i++) {
@@ -3132,8 +3184,9 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3132
3184
  const modelName = typeof modelOrName === 'string' ? modelOrName : modelOrName?.name;
3133
3185
  if (!modelName) throw new Error('Model definition in pack is missing a name.');
3134
3186
 
3135
- if (existingModelNames.includes(modelName)) {
3136
- logger.debug(`[Model Install] Skipping '${modelName}': already exists`);
3187
+ const r = typeof user?.username === 'string' && await modelsCollection.findOne({name: modelName, _user: user.username || /.*/});
3188
+ if (r ) {
3189
+ logger?.debug(`[Model Install] Skipping '${modelName}': already exists`);
3137
3190
  summary.models.skipped.push(modelName);
3138
3191
  continue;
3139
3192
  }
@@ -3160,7 +3213,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3160
3213
  await modelsCollection.insertOne(preparedModel);
3161
3214
  summary.models.installed.push(modelName);
3162
3215
  } catch (e) {
3163
- logger.error(e);
3216
+ logger?.error(e);
3164
3217
  const modelName = typeof modelOrName === 'string' ? modelOrName : modelOrName?.name || 'unknown';
3165
3218
  errors.push(`Failed to install model '${modelName}': ${e.message}`);
3166
3219
  summary.models.failed.push(modelName);
@@ -3171,8 +3224,8 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3171
3224
  // --- PHASE 2: DATA INSTALLATION ---
3172
3225
  const dataToInstall = {...pack.data?.all, ...pack.data?.[lang]};
3173
3226
  if (!dataToInstall || Object.keys(dataToInstall).length === 0) {
3174
- logger.warn(`Pack '${pack.name}' has no data to install.`);
3175
- return {success: false, summary, errors, modifiedCount: 0};
3227
+ logger?.warn(`Pack '${pack.name}' has no data to install.`);
3228
+ return {success: errors.length === 0, summary, errors, modifiedCount: 0};
3176
3229
  }
3177
3230
 
3178
3231
  // Process link references (same as original)
@@ -3205,49 +3258,49 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3205
3258
  const documents = dataToInstall[modelName];
3206
3259
  if (documents.length === 0) continue;
3207
3260
 
3208
- const docsToInsert = [];
3209
- const modelDefForHash = await getModel(modelName, user);
3261
+ try {
3262
+ const docsToInsert = [];
3263
+ const modelDefForHash = await getModel(modelName, user);
3210
3264
 
3211
- for (const docSource of documents) {
3212
- let docForInsert = {...docSource};
3265
+ for (const docSource of documents) {
3266
+ let docForInsert = {...docSource};
3213
3267
 
3214
- // Clear $link fields for first pass
3215
- for (const key in docForInsert) {
3216
- if (isPlainObject(docForInsert[key]) && docForInsert[key].$link) {
3217
- docForInsert[key] = null;
3268
+ // Clear $link fields for first pass
3269
+ for (const key in docForInsert) {
3270
+ if (isPlainObject(docForInsert[key]) && docForInsert[key].$link) {
3271
+ docForInsert[key] = null;
3272
+ }
3218
3273
  }
3219
- }
3220
3274
 
3221
- const tempId = docForInsert._temp_pack_id;
3222
- delete docForInsert._id;
3223
- delete docForInsert._temp_pack_id;
3275
+ const tempId = docForInsert._temp_pack_id;
3276
+ delete docForInsert._id;
3277
+ delete docForInsert._temp_pack_id;
3224
3278
 
3225
- if (user) docForInsert._user = username;
3226
-
3227
- docForInsert._model = modelName;
3228
- docForInsert._hash = getFieldValueHash(modelDefForHash, docForInsert);
3279
+ if (user) docForInsert._user = username;
3229
3280
 
3230
- // Check for existing document
3231
- const existingQuery = {
3232
- _hash: docForInsert._hash,
3233
- _model: modelName
3234
- };
3281
+ docForInsert._model = modelName;
3282
+ docForInsert._hash = getFieldValueHash(modelDefForHash, docForInsert);
3283
+
3284
+ // Check for existing document
3285
+ const existingQuery = {
3286
+ _hash: docForInsert._hash,
3287
+ _model: modelName
3288
+ };
3235
3289
 
3236
- if (docForInsert._env) existingQuery._env = docForInsert._env;
3237
- if (user) existingQuery._user = username;
3290
+ if (docForInsert._env) existingQuery._env = docForInsert._env;
3291
+ if (user) existingQuery._user = username;
3238
3292
 
3239
- const existingDoc = await collection.findOne(existingQuery, {projection: {_id: 1}});
3240
- if (existingDoc) {
3241
- tempIdToNewIdMap[tempId] = existingDoc._id;
3242
- summary.datas.skipped++;
3243
- } else {
3244
- docForInsert._temp_pack_id_for_mapping = tempId;
3245
- docsToInsert.push(docForInsert);
3293
+ const existingDoc = await collection.findOne(existingQuery, {projection: {_id: 1}});
3294
+ if (existingDoc) {
3295
+ tempIdToNewIdMap[tempId] = existingDoc._id;
3296
+ summary.datas.skipped++;
3297
+ } else {
3298
+ docForInsert._temp_pack_id_for_mapping = tempId;
3299
+ docsToInsert.push(docForInsert);
3300
+ }
3246
3301
  }
3247
- }
3248
3302
 
3249
- if (docsToInsert.length > 0) {
3250
- try {
3303
+ if (docsToInsert.length > 0) {
3251
3304
  const finalDocsToInsert = docsToInsert.map(d => {
3252
3305
  const doc = {...d};
3253
3306
  delete doc._temp_pack_id_for_mapping;
@@ -3265,22 +3318,22 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3265
3318
  tempIdToNewIdMap[doc._temp_pack_id_for_mapping] = result.insertedIds[index];
3266
3319
  }
3267
3320
  });
3268
- } catch (e) {
3269
- summary.datas.failed += docsToInsert.length;
3270
- errors.push(`Error inserting batch for ${modelName}: ${e.message}`);
3271
- logger.error(`[Pack Install] Error on insertMany for model ${modelName}:`, e);
3272
3321
  }
3322
+ } catch (e) {
3323
+ summary.datas.failed += documents.length;
3324
+ errors.push(`Error processing model ${modelName}: ${e.message}`);
3325
+ logger?.error(`[Pack Install] Error processing model ${modelName}:`, e);
3273
3326
  }
3274
3327
  }
3275
3328
 
3276
3329
  // --- PASS 2: REFERENCE LINKING ---
3277
- logger.info(`[Pack Install] Starting Pass 2: Linking ${linkQueue.length} references`);
3330
+ logger?.info(`[Pack Install] Starting Pass 2: Linking ${linkQueue.length} references`);
3278
3331
  for (const linkOp of linkQueue) {
3279
3332
  const {sourceTempId, sourceModelName, fieldName, linkSelector} = linkOp;
3280
3333
  const sourceId = tempIdToNewIdMap[sourceTempId];
3281
3334
 
3282
3335
  if (!sourceId) {
3283
- logger.warn(`[LINK FAILED] Could not find newly inserted document for temp ID ${sourceTempId}. Skipping link.`);
3336
+ logger?.warn(`[LINK FAILED] Could not find newly inserted document for temp ID ${sourceTempId}. Skipping link.`);
3284
3337
  continue;
3285
3338
  }
3286
3339
 
@@ -3292,7 +3345,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3292
3345
  const fieldDef = sourceModelDef.fields.find(f => f.name === fieldName);
3293
3346
 
3294
3347
  if (!fieldDef) {
3295
- logger.warn(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}'`);
3348
+ logger?.warn(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}'`);
3296
3349
  errors.push(`[LINK FAILED] Field '${fieldName}' not found in source model '${sourceModelName}'`);
3297
3350
  summary.datas.failed++;
3298
3351
  continue;
@@ -3306,7 +3359,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3306
3359
 
3307
3360
  if (!targetDocs || targetDocs.length === 0) {
3308
3361
  const errorMsg = `[LINK FAILED] No target found for ${JSON.stringify(linkSelector)}`;
3309
- logger.warn(errorMsg);
3362
+ logger?.warn(errorMsg);
3310
3363
  errors.push(errorMsg);
3311
3364
  summary.datas.failed++;
3312
3365
  continue;
@@ -3325,7 +3378,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3325
3378
 
3326
3379
  } catch (e) {
3327
3380
  const errorMsg = `[LINK CRITICAL] Error linking ${sourceModelName}.${fieldName}: ${e.message}`;
3328
- logger.error(errorMsg, e.stack);
3381
+ logger?.error(errorMsg, e.stack);
3329
3382
  errors.push(errorMsg);
3330
3383
  summary.datas.failed++;
3331
3384
  }
@@ -3334,7 +3387,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3334
3387
  if (options.installForUser && user?.username) {
3335
3388
  if (pack.name)
3336
3389
  await packsCollection.deleteOne({name: pack.name, _user: user.username});
3337
- logger.info(`--- Creating pack '${pack.name}' for user... ---`);
3390
+ logger?.info(`--- Creating pack '${pack.name}' for user... ---`);
3338
3391
  const packToCreate = {...pack, _id: undefined, private: true, _user: user.username};
3339
3392
  await packsCollection.insertOne(packToCreate);
3340
3393
  }
@@ -3345,7 +3398,7 @@ export async function installPack(packIdentifier, user = null, lang = 'en', opti
3345
3398
  }
3346
3399
 
3347
3400
  const modifiedCount = summary.datas.inserted + summary.datas.updated;
3348
- logger.info(`--- ${logPrefix} completed ---`);
3401
+ logger?.info(`--- ${logPrefix} completed ---`);
3349
3402
  return {
3350
3403
  success: errors.length === 0,
3351
3404
  summary,
@@ -534,7 +534,7 @@ function prepareDocument(doc, model, me) {
534
534
  }
535
535
 
536
536
  docToProcess._model = model.name;
537
- docToProcess._user = me._user || me.username;
537
+ docToProcess._user = me.username || me._user;
538
538
  docToProcess._hash = getFieldValueHash(model, docToProcess);
539
539
 
540
540
  return docToProcess;
@@ -581,7 +581,8 @@ export const checkHash = async (me, model, hash, excludeId = null) => {
581
581
  const query = {
582
582
  _model: model.name,
583
583
  _hash: hash,
584
- ...(excludeId && {_id: {$ne: new ObjectId(excludeId)}})
584
+ ...(excludeId && {_id: {$ne: new ObjectId(excludeId)}}),
585
+ _user: me.username
585
586
  };
586
587
 
587
588
  console.log("Query being executed:", JSON.stringify(query, null, 2));