data-primals-engine 1.6.2 → 1.6.5

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 (39) hide show
  1. package/README.md +56 -911
  2. package/client/package-lock.json +64 -50
  3. package/client/package.json +6 -0
  4. package/client/src/App.scss +29 -0
  5. package/client/src/Dashboard.jsx +1 -1
  6. package/client/src/WorkflowEditor.jsx +101 -0
  7. package/client/src/WorkflowEditor.scss +29 -4
  8. package/client/src/_variables.scss +3 -0
  9. package/doc/automation-workflows.md +102 -0
  10. package/doc/core-concepts.md +33 -0
  11. package/doc/custom-api-endpoints.md +40 -0
  12. package/doc/dashboards-kpis-charts.md +49 -0
  13. package/doc/data-management.md +120 -0
  14. package/doc/data-models.md +75 -0
  15. package/doc/index.md +14 -0
  16. package/doc/roles-permissions.md +43 -0
  17. package/doc/users.md +30 -0
  18. package/package.json +7 -5
  19. package/src/client.js +6 -4
  20. package/src/core.js +31 -12
  21. package/src/defaultModels +1628 -0
  22. package/src/defaultModels.js +14 -0
  23. package/src/filter.js +110 -42
  24. package/src/modules/data/data.history.js +5 -1
  25. package/src/modules/data/data.js +2 -1
  26. package/src/modules/data/data.operations.js +116 -72
  27. package/src/modules/data/data.relations.js +1 -1
  28. package/src/modules/mongodb.js +2 -1
  29. package/src/modules/swagger.js +25 -5
  30. package/src/modules/user.js +138 -76
  31. package/src/modules/workflow.js +187 -177
  32. package/src/providers.js +1 -1
  33. package/swagger-en.yml +2308 -472
  34. package/swagger-fr.yml +487 -3
  35. package/test/core.test.js +341 -0
  36. package/test/data.history.integration.test.js +140 -16
  37. package/test/data.integration.test.js +137 -2
  38. package/test/user.test.js +201 -280
  39. package/test/workflow.integration.test.js +8 -0
@@ -14,6 +14,14 @@ export const defaultModels = {
14
14
  {
15
15
  "name": "description",
16
16
  "type": "richtext"
17
+ },
18
+ {
19
+ "name": "filter",
20
+ "type": "code",
21
+ "language": "json",
22
+ "conditionBuilder": true,
23
+ "required": false,
24
+ "hint": "Filtre JSON qui restreint la portée de cette permission. Le modèle cible est déduit du nom de la permission (ex: 'product.edit')."
17
25
  }
18
26
  ]
19
27
  },
@@ -1111,6 +1119,12 @@ export const defaultModels = {
1111
1119
  hint: "The current status of the workflow execution."
1112
1120
  },
1113
1121
  { name: 'stepExecutionsCount', type: 'object' },
1122
+ {
1123
+ name: 'history',
1124
+ type: 'array',
1125
+ itemsType: 'object',
1126
+ hint: "Stores the detailed execution history of each step and action."
1127
+ },
1114
1128
  { name: 'currentStep', type: 'relation', relation: 'workflowStep', hint: "The step currently being executed or waited on." },
1115
1129
  { name: 'owner', type: 'relation', relation: 'user', required: false },
1116
1130
  { name: 'startedAt', type: 'datetime', required: true, hint: "Timestamp when the workflow run began." },
package/src/filter.js CHANGED
@@ -9,6 +9,83 @@ export function setSafeRegex(validator) {
9
9
  safeRegex = validator;
10
10
  }
11
11
 
12
+ /**
13
+ * Évalue une comparaison entre une valeur cible et une valeur de condition.
14
+ * @param {string} operator - L'opérateur de comparaison (ex: '$eq', '$lt').
15
+ * @param {*} targetValue - La valeur actuelle du champ dans les données.
16
+ * @param {*} processedConditionValue - La valeur de la condition à comparer.
17
+ * @param {object} condition - La condition complète pour le contexte de logging.
18
+ * @returns {boolean} - Le résultat de la comparaison.
19
+ */
20
+ function evaluateComparison(operator, targetValue, processedConditionValue, condition) {
21
+ const logClientEvalWarning = (message, details) => {
22
+ console.warn(`[Client Eval] ${message}:`, details);
23
+ };
24
+
25
+ try {
26
+ switch (operator) {
27
+ case '$eq': return targetValue == processedConditionValue; // Utilise '==' pour une comparaison plus souple
28
+ case '$ne': return targetValue != processedConditionValue;
29
+ case '$gt': return targetValue > processedConditionValue;
30
+ case '$lt': return targetValue < processedConditionValue;
31
+ case '$gte': return targetValue >= processedConditionValue;
32
+ case '$lte': return targetValue <= processedConditionValue;
33
+ case '$regex':
34
+ if (typeof targetValue !== 'string' || !processedConditionValue || typeof processedConditionValue !== 'string') return false;
35
+ try {
36
+ if (safeRegex && !safeRegex(processedConditionValue)) return false;
37
+ const regex = new RegExp(processedConditionValue, 'i');
38
+ return regex.test(targetValue);
39
+ } catch (e) {
40
+ logClientEvalWarning(`Invalid regex pattern: ${processedConditionValue}`, condition);
41
+ return false;
42
+ }
43
+ case '$in': return Array.isArray(processedConditionValue) && processedConditionValue.includes(targetValue);
44
+ case '$nin': return !Array.isArray(processedConditionValue) || !processedConditionValue.includes(targetValue);
45
+ default: return true; // Permissif par défaut pour les opérateurs non gérés
46
+ }
47
+ } catch (evalError) {
48
+ logClientEvalWarning(`Error during condition evaluation: ${operator}, targetValue=${targetValue}, processedConditionValue=${processedConditionValue}`, condition);
49
+ return false;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Récupère une valeur imbriquée dans un objet en utilisant une chaîne de chemin.
55
+ * Gère les tableaux et les objets. Retourne undefined si le chemin n'est pas trouvé.
56
+ * Exemple: getNestedValue({ a: { b: [ { c: 1 } ] } }, 'a.b.0.c') -> 1
57
+ *
58
+ * @param {object} obj L'objet source.
59
+ * @param {string} path La chaîne de chemin (ex: 'user.address.city').
60
+ * @returns {*} La valeur trouvée ou undefined.
61
+ */
62
+ function getNestedValue(obj, path) {
63
+ // Vérifie si l'objet ou le chemin est invalide
64
+ if (!obj || typeof path !== 'string') {
65
+ return undefined;
66
+ }
67
+ // Sépare le chemin en clés individuelles (ex: 'a.b.0.c' -> ['a', 'b', '0', 'c'])
68
+ const keys = path.split('.');
69
+ let current = obj; // Commence à la racine de l'objet
70
+
71
+ // Parcourt chaque clé dans le chemin
72
+ for (const key of keys) {
73
+ // Si à un moment donné on atteint null ou undefined, le chemin est invalide
74
+ if (current === null || current === undefined) {
75
+ return undefined;
76
+ }
77
+ // Récupère la valeur pour la clé actuelle
78
+ const value = current[key];
79
+ // Si la valeur est undefined, le chemin est invalide
80
+ if (value === undefined) {
81
+ return undefined;
82
+ }
83
+ // Passe au niveau suivant de l'objet/tableau
84
+ current = value;
85
+ }
86
+ // Retourne la valeur finale trouvée
87
+ return current;
88
+ }
12
89
  /**
13
90
  * Evaluates a single condition against form data.
14
91
  * @param {object} currentModelDef - The definition of the current model.
@@ -27,6 +104,17 @@ const evaluateSingleCondition = (currentModelDef, condition, formData, allModels
27
104
  return true; // Permissive default
28
105
  }
29
106
 
107
+ // If the condition is a standard query operator like { amount: { $lt: 1000 } }
108
+ const fieldNameFromCondition = Object.keys(condition)[0];
109
+ const conditionValueObject = condition[fieldNameFromCondition];
110
+
111
+ if (typeof conditionValueObject === 'object' && conditionValueObject !== null && !Array.isArray(conditionValueObject)) {
112
+ const operator = Object.keys(conditionValueObject)[0];
113
+ if (operator.startsWith('$')) {
114
+ return evaluateComparison(operator, formData[fieldNameFromCondition], conditionValueObject[operator], condition, checkRegex);
115
+ }
116
+ }
117
+
30
118
  // Si la condition est de la forme {field: value}, on la transforme en {$eq: value}
31
119
  if (!Object.keys(condition)[0].startsWith('$') && typeof condition[Object.keys(condition)[0]] !== 'object') {
32
120
  const fieldName = Object.keys(condition)[0];
@@ -41,12 +129,12 @@ const evaluateSingleCondition = (currentModelDef, condition, formData, allModels
41
129
  const [fieldPath, expectedValue] = condition[operator];
42
130
  if (typeof fieldPath === 'string' && fieldPath.startsWith('$')) {
43
131
  const fieldName = fieldPath.substring(1); // Enlève le $ devant
44
- const actualValue = formData[fieldName];
45
- if (operator === '$eq') return actualValue == expectedValue;
46
- if (operator === '$ne') return actualValue != expectedValue;
132
+ const actualValue = getNestedValue(formData, fieldName);
133
+ return evaluateComparison(operator, actualValue, expectedValue, condition, checkRegex);
47
134
  }
48
135
  }
49
136
 
137
+
50
138
  // Si la condition contient des opérateurs logiques, on les gère ici
51
139
  if (condition.$and || condition.$or || condition.$not || condition.$nor) {
52
140
  console.warn("[Client Eval] Condition logique détectée dans evaluateSingleCondition, ce n'est pas attendu. Devrait être géré par isConditionMet.");
@@ -125,7 +213,7 @@ const evaluateSingleCondition = (currentModelDef, condition, formData, allModels
125
213
  return false;
126
214
  }
127
215
 
128
- return evaluateComparison(fieldValue, targetValue, processedConditionValue, condition);
216
+ return evaluateComparison('$eq', targetValue, processedConditionValue, condition);
129
217
 
130
218
  function logClientEvalWarning(message, details) {
131
219
  console.warn(`[Client Eval] ${message}:`, details);
@@ -149,43 +237,6 @@ const evaluateSingleCondition = (currentModelDef, condition, formData, allModels
149
237
  }
150
238
  }
151
239
 
152
- function evaluateComparison(operator, targetValue, processedConditionValue, condition) {
153
- try {
154
- switch (typeof operator === 'object' ? Object.keys(operator)[0] : null) {
155
- case '$eq': return targetValue === processedConditionValue;
156
- case '$ne': return targetValue !== processedConditionValue;
157
- case '$gt': return targetValue > processedConditionValue;
158
- case '$lt': return targetValue < processedConditionValue;
159
- case '$gte': return targetValue >= processedConditionValue;
160
- case '$lte': return targetValue <= processedConditionValue;
161
- case '$regex':
162
- if (typeof targetValue !== 'string') return false;
163
- if (typeof processedConditionValue !== 'string') return false;
164
- try {
165
- // On vérifie si la fonction a été injectée ET si la regex est sûre.
166
- // Côté client, `safeRegex` sera null, donc la vérification est simplement ignorée.
167
- if( checkRegex && safeRegex && safeRegex(processedConditionValue)) {
168
- const regex = new RegExp(processedConditionValue, 'i');
169
- return regex.test(targetValue);
170
- }
171
- return false;
172
- } catch (e) {
173
- logClientEvalWarning(`Invalid regex pattern: ${processedConditionValue}`, condition);
174
- return false;
175
- }
176
- case '$in':
177
- return Array.isArray(processedConditionValue) && processedConditionValue.includes(String(targetValue));
178
- case '$nin':
179
- return !Array.isArray(processedConditionValue) || !processedConditionValue.includes(String(targetValue));
180
- default:
181
- logClientEvalWarning(`Unhandled operator in client evaluation logic: ${operator}`, condition);
182
- return true; // Permissive default
183
- }
184
- } catch (evalError) {
185
- logClientEvalWarning(`Error during client condition evaluation: ${operator}, targetValue=${targetValue}, processedConditionValue=${processedConditionValue}`, condition);
186
- return false;
187
- }
188
- }
189
240
  };
190
241
 
191
242
  export const isConditionMet = (model, cond, formData, allModels, user,checkRegex=true) => {
@@ -197,6 +248,22 @@ export const isConditionMet = (model, cond, formData, allModels, user,checkRegex
197
248
  return true;
198
249
  }
199
250
 
251
+ // --- NOUVELLE LOGIQUE ---
252
+ // Gère les conditions de type expression d'agrégation comme { "$lt": ["$amount", 1000] }
253
+ const operatorKeys = Object.keys(condition).filter(k => k.startsWith('$'));
254
+ if (operatorKeys.length === 1) {
255
+ const operator = operatorKeys[0];
256
+ const operands = condition[operator];
257
+
258
+ if (Array.isArray(operands) && operands.length === 2 && typeof operands[0] === 'string' && operands[0].startsWith('$')) {
259
+ const fieldName = operands[0].substring(1); // Extrait 'amount' de '$amount'
260
+ const expectedValue = operands[1];
261
+ const actualValue = getNestedValue(formData, fieldName);
262
+
263
+ return evaluateComparison(operator, actualValue, expectedValue, condition, checkRegex);
264
+ }
265
+ }
266
+
200
267
  // Cas spécial: Évaluation sans modèle (ex: pour les webhooks où la condition est déjà résolue)
201
268
  // Dans ce mode, la condition doit être un objet avec un seul opérateur.
202
269
  if (!model) {
@@ -272,4 +339,5 @@ export const isConditionMet = (model, cond, formData, allModels, user,checkRegex
272
339
  }
273
340
 
274
341
  return evaluateSingleCondition(model, condition, formData, allModels, user, checkRegex);
275
- };
342
+ };
343
+
@@ -391,7 +391,11 @@ export function onInit(defaultEngine) {
391
391
  }
392
392
 
393
393
  // If this is the first history record for this document, add a snapshot of the 'before' state.
394
- if (!lastVersionDoc) {
394
+ // --- FIX ---
395
+ // The check should be based on the calculated version number, not on the existence of lastVersionDoc,
396
+ // which could be affected by race conditions in a busy environment. If we are creating version 1,
397
+ // it means no prior history exists for this document, so we must add the snapshot.
398
+ if (newVersion === 1) {
395
399
  historyEntry.snapshot = beforeDoc;
396
400
  logger.debug(`History v${newVersion} (update with initial snapshot) created for ${modelName} document ${afterDoc._id}`);
397
401
  } else {
@@ -14,7 +14,7 @@ import i18n from "../../i18n.js";
14
14
  import checkDiskSpace from "check-disk-space";
15
15
  import {removeFile} from "../file.js";
16
16
  import {hasPermission} from "../user.js";
17
- import {profiles} from "../../../client/src/constants.js";
17
+ import { onInit as userInit } from '../user.js';
18
18
  import {registerRoutes} from "./data.routes.js";
19
19
  import {mongoDBWhitelist} from "./data.core.js";
20
20
  import {onInit as relationsInit} from "./data.relations.js";
@@ -139,6 +139,7 @@ export async function onInit(defaultEngine) {
139
139
  validationInit(defaultEngine);
140
140
  relationsInit(defaultEngine);
141
141
  scheduleInit(defaultEngine);
142
+ userInit(defaultEngine);
142
143
  operationsInit(defaultEngine);
143
144
 
144
145
 
@@ -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));
@@ -693,14 +694,27 @@ export const getModel = async (modelName, user) => {
693
694
  export const getModels = async () => {
694
695
  return await getCollection('models')?.find({'$or': [{_user: {$exists: false}}]}).toArray() || [];
695
696
  }
696
- export const insertData = async (modelName, data, files, user, triggerWorkflow = true, waitForWorkflow = true) => {
697
+ export const insertData = async (modelName, data, files, user, triggerWorkflow = true, waitForWorkflow = true, checkPermissions = true) => {
697
698
 
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};
699
+ if (checkPermissions) {
700
+ // --- Vérification des permissions ---
701
+ const permissionFilter = await hasPermission(["API_ADMIN", "API_ADD_DATA", "API_ADD_DATA_" + modelName], user);
702
+ const hasDeniedPermission = await hasPermission(["API_ADD_DATA_NOT_" + modelName], user);
703
+
704
+ if (!(isDemoUser(user) && Config.Get("useDemoAccounts")) && isLocalUser(user) && (!permissionFilter || hasDeniedPermission)) {
705
+ return { success: false, error: i18n.t('api.permission.addData', "You do not have permission to add this data."), statusCode: 403 };
706
+ }
707
+
708
+ // Si un filtre de permission existe, valider les données entrantes
709
+ if (typeof permissionFilter === 'object' && Object.keys(permissionFilter).length > 0) {
710
+ const dataArray = Array.isArray(data) ? data : [data];
711
+ const model = await getModel(modelName, user);
712
+ for (const doc of dataArray) {
713
+ if (!isConditionMet(model, permissionFilter, doc, [], user)) {
714
+ 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 };
715
+ }
716
+ }
717
+ }
704
718
  }
705
719
 
706
720
  const collection = await getCollectionForUser(user);
@@ -1084,27 +1098,40 @@ export const editData = async (modelName, filter, data, files, user, triggerWork
1084
1098
  const internalEditOrPatchData = async (modelName, filter, data, files, user, isPatch, triggerWorkflow = true, waitForWorkflow = false) => {
1085
1099
  try {
1086
1100
  // 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"));
1101
+ const permissionResult = await hasPermission(["API_ADMIN", "API_EDIT_DATA", "API_EDIT_DATA_" + modelName], user);
1102
+ const hasDeniedPermission = await hasPermission(["API_EDIT_DATA_NOT_" + modelName], user);
1103
+
1104
+ if (user.username !== 'demo' && isLocalUser(user) && (!permissionResult || hasDeniedPermission)) {
1105
+ throw new Error(i18n.t("api.permission.editData", "You do not have permission to edit this data."));
1091
1106
  }
1092
1107
 
1108
+
1093
1109
  const collection = await getCollectionForUser(user);
1094
- const model = await modelsCollection.findOne({name: modelName, _user: user.username});
1110
+ const model = await modelsCollection.findOne({$and : [{name: modelName}, {$or: [{_user: user._user}, {_user:user.username}]}]});
1095
1111
  if (!model) {
1096
1112
  throw new Error(i18n.t("api.model.notFound", {model: modelName}));
1097
1113
  }
1098
1114
 
1099
- // 2. Récupération des documents existants et de leur hash original
1100
- const existingDocs = (await searchData({model: modelName, filter}, user))?.data;
1115
+ // 2. Combinaison du filtre de la requête et du filtre de permission
1116
+ let combinedFilter = filter;
1117
+ if (permissionResult && typeof permissionResult === 'object' && Object.keys(permissionResult).length > 0) {
1118
+ combinedFilter = {
1119
+ $and: [filter, permissionResult]
1120
+ };
1121
+ }
1122
+
1123
+ // 3. Récupération des documents à modifier en utilisant le filtre combiné
1124
+ const docsToEditQuery = await searchData({model: modelName, filter: combinedFilter}, user);
1125
+ let existingDocs = docsToEditQuery.data;
1126
+
1101
1127
  if (!existingDocs || existingDocs.length === 0) {
1102
- return {success: false, error: i18n.t("api.data.notFound")};
1128
+ throw new Error(i18n.t("api.data.notFound"));
1103
1129
  }
1130
+
1104
1131
  const ids = existingDocs.map(d => new ObjectId(d._id));
1105
1132
  const originalHash = existingDocs[0]._hash; // Sauvegarde du hash avant modification
1106
1133
 
1107
- // 3. Préparation des données de mise à jour en séparant les opérateurs
1134
+ // 4. Préparation des données de mise à jour en séparant les opérateurs
1108
1135
  const updateSetData = {};
1109
1136
  const updatePushData = {};
1110
1137
 
@@ -1164,15 +1191,10 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1164
1191
  }
1165
1192
  }
1166
1193
 
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
- }
1194
+ // 5. Validation adaptée pour patch ou edit
1195
+ await validateModelData(isPatch ? updateSetData : { ...existingDocs[0], ...updateSetData }, model, isPatch);
1174
1196
 
1175
- // 4.1 Validation pour les opérations $push
1197
+ // 5.1 Validation pour les opérations $push
1176
1198
  for (const fieldName in updatePushData) {
1177
1199
  const field = model.fields.find(f => f.name === fieldName);
1178
1200
  if (!field) throw new Error(`Le champ '${fieldName}' pour l'opération $push n'existe pas dans le modèle '${model.name}'.`);
@@ -1197,7 +1219,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1197
1219
  }
1198
1220
 
1199
1221
 
1200
- // 5. Vérification des champs uniques (s'applique uniquement aux opérations $set)
1222
+ // 6. Vérification des champs uniques (s'applique uniquement aux opérations $set)
1201
1223
  const uniqueFields = model.fields.filter(f => f.unique);
1202
1224
  for (const field of uniqueFields) {
1203
1225
  if (updateData[field.name] !== undefined) {
@@ -1216,7 +1238,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1216
1238
  }
1217
1239
  }
1218
1240
 
1219
- // 6. Traitement des relations (pour $set)
1241
+ // 7. Traitement des relations (pour $set)
1220
1242
  const relationFields = model.fields.filter(f => f.type === 'relation');
1221
1243
  for (const field of relationFields) {
1222
1244
  if (updateData[field.name] !== undefined) {
@@ -1240,7 +1262,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1240
1262
  }
1241
1263
  }
1242
1264
 
1243
- // 7. Application des filtres de champ (pour $set)
1265
+ // 8. Application des filtres de champ (pour $set)
1244
1266
  for (const field of model.fields) {
1245
1267
  // On saute les champs 'file' car ils ont déjà été traités
1246
1268
  if (field.type !== 'file' && field.itemsType !== 'file' && updateData[field.name] !== undefined && dataTypes[field.type]?.filter) {
@@ -1286,7 +1308,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1286
1308
  }
1287
1309
  }
1288
1310
 
1289
- // 8. Vérification des limites de stockage avant la mise à jour
1311
+ // 9. Vérification des limites de stockage avant la mise à jour
1290
1312
  const originalDocsSize = calculateDataSize(existingDocs);
1291
1313
  const finalStateForSize = existingDocs.map(doc => {
1292
1314
  const updatedDoc = { ...doc, ...updateSetData };
@@ -1323,7 +1345,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1323
1345
  }
1324
1346
  }
1325
1347
 
1326
- // 8. Calcul du nouveau hash en simulant l'état final
1348
+ // 10. Calcul du nouveau hash en simulant l'état final
1327
1349
  const finalStateForHash = { ...existingDocs[0], ...updateSetData };
1328
1350
  for (const fieldName in updatePushData) {
1329
1351
  let itemsToAdd;
@@ -1358,7 +1380,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1358
1380
  }
1359
1381
  }
1360
1382
 
1361
- // 9. *** CORRECTION LOGIQUE ***
1383
+ // 11. *** CORRECTION LOGIQUE ***
1362
1384
  // On ne vérifie l'unicité que si le hash a réellement changé.
1363
1385
  if (newHash !== originalHash) {
1364
1386
  const hashCheck = await checkHash(user, model, newHash, existingDocs[0]._id.toString());
@@ -1368,7 +1390,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1368
1390
  }
1369
1391
  }
1370
1392
 
1371
- // 10. Exécution de la mise à jour.
1393
+ // 12. Exécution de la mise à jour.
1372
1394
  // Si une opération $push est présente, nous devons utiliser une pipeline d'agrégation
1373
1395
  // pour gérer de manière robuste le cas où le champ cible est `null` ou n'existe pas.
1374
1396
  let updateCommand;
@@ -1430,7 +1452,7 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1430
1452
  });
1431
1453
  }
1432
1454
 
1433
- // 11. Tâches post-mise à jour (schedules, workflows) (inchangé)
1455
+ // 13. Tâches post-mise à jour (schedules, workflows) (inchangé)
1434
1456
  if (["workflowTrigger", "alert"].includes(modelName)) {
1435
1457
  await handleScheduledJobs(modelName, existingDocs, collection, finalUpdateOperation.$set || {});
1436
1458
  }
@@ -1459,40 +1481,65 @@ const internalEditOrPatchData = async (modelName, filter, data, files, user, isP
1459
1481
  export const deleteData = async (modelName, filter, user = {}, triggerWorkflow, waitForWorkflow = false) => {
1460
1482
 
1461
1483
  try {
1462
- const collection = await getCollectionForUser(user);
1484
+ const permissionFilter = await hasPermission(["API_ADMIN", "API_DELETE_DATA", "API_DELETE_DATA_" + modelName], user);
1485
+ const hasDeniedPermission = await hasPermission(["API_DELETE_DATA_NOT_" + modelName], user);
1463
1486
 
1464
- // --- Début de la logique de suppression ---
1487
+ if (user?.username !== 'demo' && isLocalUser(user) && (!permissionFilter || hasDeniedPermission)) {
1488
+ // Si l'utilisateur n'a pas la permission de base, on arrête tout de suite.
1489
+ return { success: false, error: i18n.t("api.permission.deleteData", "You do not have permission to delete this data."), deletedCount: 0 };
1490
+ }
1465
1491
 
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
- });
1492
+ const collection = await getCollectionForUser(user);
1472
1493
 
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
- }
1494
+ // 1. Construire le filtre de recherche.
1495
+ // Nous allons utiliser un seul objet $match avec un $and global pour combiner toutes les conditions.
1496
+ const matchConditions = [];
1477
1497
 
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
- });
1498
+ // Conditions de base (modèle et utilisateur)
1499
+ if (user) {
1500
+ matchConditions.push({ $expr: { '$eq': ["$_user", user.username] } });
1501
+ }
1502
+ if (modelName) {
1503
+ matchConditions.push({ _model: modelName });
1504
+ }
1483
1505
 
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 {
1506
+ // Filtre de la requête (par ID ou autre)
1507
+ if (Array.isArray(filter) && filter.length > 0 && isObjectId(filter[0])) {
1508
+ // Filtre par tableau d'IDs
1509
+ matchConditions.push({ _id: { "$in": filter.map(m => new ObjectId(m)) } });
1510
+ } else if (filter && typeof filter === 'object' && Object.keys(filter).length > 0) {
1511
+ const firstKey = Object.keys(filter)[0];
1512
+ // Si le filtre est une expression d'agrégation (commence par $), on l'encapsule dans $expr
1513
+ if (firstKey.startsWith('$')) {
1514
+ matchConditions.push({ $expr: filter });
1515
+ } else {
1516
+ // Sinon, on le traite comme un filtre de document standard
1517
+ const processedFilter = {};
1518
+ for (const key in filter) {
1519
+ if (key === '_id' && !isObjectId(filter[key])) {
1520
+ // S'assurer que la conversion ne se fait que si la chaîne est un ID valide
1521
+ if (ObjectId.isValid(filter[key])) {
1522
+ processedFilter[key] = new ObjectId(filter[key]);
1523
+ } else {
1524
+ processedFilter[key] = filter[key]; // Garder la valeur originale si non valide
1525
+ }
1526
+ } else {
1527
+ processedFilter[key] = filter[key];
1528
+ }
1529
+ }
1530
+ matchConditions.push(processedFilter);
1531
+ }
1532
+ }
1491
1533
 
1534
+ // Filtre de permission
1535
+ if (permissionFilter && typeof permissionFilter === 'object') {
1536
+ matchConditions.push(permissionFilter);
1492
1537
  }
1493
1538
 
1539
+ const findFilter = matchConditions.length > 0 ? { $and: matchConditions } : {};
1540
+
1494
1541
  // 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();
1542
+ const documentsToDelete = await collection.aggregate([{ $match: findFilter }]).toArray();
1496
1543
 
1497
1544
  if (documentsToDelete.length === 0) {
1498
1545
  logger.info(`[deleteData] No documents found matching the criteria for user ${user?.username}.`);
@@ -1531,15 +1578,6 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
1531
1578
  await Promise.allSettled(deletePromises);
1532
1579
  }
1533
1580
 
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
1581
  // *** Ajout de l'annulation du schedule pour workflowTrigger ***
1544
1582
  if (docToDelete._model === 'workflowTrigger') {
1545
1583
  const jobId = `workflowTrigger_${docToDelete._id}`;
@@ -1679,9 +1717,15 @@ export const deleteData = async (modelName, filter, user = {}, triggerWorkflow,
1679
1717
  logger.info(`[deleteData] No documents to delete for user ${user?.username} after permission checks or matching criteria.`);
1680
1718
  }
1681
1719
 
1682
- const res = {success: true, deletedCount}
1683
- const plugin = await Event.Trigger("OnDataDeleted", "event", "system", engine, {model: modelName, filter});
1684
- await Event.Trigger("OnDataDeleted", "event", "user", {model: modelName, filter});
1720
+ const res = { success: true, deletedCount };
1721
+
1722
+ // --- CORRECTION ---
1723
+ // The event payload must match what the history listener expects: { modelName, user, before }.
1724
+ // 'documentsToDelete' contains the full documents before they were deleted.
1725
+ const eventPayload = { modelName: modelNameToInvalidate, user, before: documentsToDelete };
1726
+ const plugin = await Event.Trigger("OnDataDeleted", "event", "system", engine, eventPayload);
1727
+ await Event.Trigger("OnDataDeleted", "event", "user", eventPayload);
1728
+
1685
1729
  return plugin || res;
1686
1730
 
1687
1731
  } catch (error) {
@@ -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;
@@ -2,6 +2,7 @@
2
2
  import {Logger} from "../gameObject.js";
3
3
  import {MongoDatabase} from "../engine.js";
4
4
  import {ObjectId} from "mongodb";
5
+ import {isLocalUser} from "../data.js";
5
6
 
6
7
  export let modelsCollection, datasCollection, filesCollection, packsCollection;
7
8
 
@@ -51,7 +52,7 @@ export const getCollection = (str) => {
51
52
  // New function to determine the collection name for a user
52
53
  export const getUserCollectionName = async (user) => {
53
54
  const feat = await engine.userProvider.hasFeature(user, 'indexes');
54
- return feat ? `datas_${user.username}` : 'datas';
55
+ return feat ? (isLocalUser(user) ? `datas_${user._user}` :`datas_${user.username}` ) : 'datas';
55
56
  };
56
57
 
57
58
 
@@ -10,9 +10,29 @@ let engine, logger;
10
10
  export async function onInit(defaultEngine) {
11
11
  engine = defaultEngine;
12
12
  logger = engine.getComponent(Logger);
13
-
14
- const swaggerDocument = YAML.parse(fs.readFileSync(process.cwd() + '/swagger-en.yml', 'utf8'));
15
-
16
- engine.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
17
-
13
+
14
+ const swaggerDocs = {
15
+ en: YAML.parse(fs.readFileSync(process.cwd() + '/swagger-en.yml', 'utf8')),
16
+ fr: YAML.parse(fs.readFileSync(process.cwd() + '/swagger-fr.yml', 'utf8'))
17
+ };
18
+
19
+ // Sers les assets de swagger-ui
20
+ engine.use('/api-docs', swaggerUi.serve);
21
+
22
+ // Route pour la langue par défaut (ex: /api-docs)
23
+ engine.get('/api-docs', (req, res) => {
24
+ return swaggerUi.setup(swaggerDocs['en'])(req, res);
25
+ });
26
+
27
+ // Route pour une langue spécifique (ex: /api-docs/fr)
28
+ engine.get('/api-docs/:lang', (req, res) => {
29
+ const lang = req.params.lang;
30
+
31
+ // Si la langue demandée n'existe pas, on redirige vers la version anglaise.
32
+ if (!swaggerDocs[lang]) {
33
+ return res.redirect('/api-docs/en');
34
+ }
35
+
36
+ return swaggerUi.setup(swaggerDocs[lang])(req, res);
37
+ });
18
38
  };