data-primals-engine 1.6.3 → 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.
package/src/filter.js CHANGED
@@ -1,7 +1,3 @@
1
- import {getCollectionForUser, isObjectId} from "./modules/mongodb.js";
2
- import {getModel} from "./modules/data/index.js";
3
- import {ObjectId} from "mongodb";
4
- import {safeAssignObject} from "./core.js";
5
1
 
6
2
  let safeRegex = null; // Par défaut, aucune fonction de validation n'est définie.
7
3
 
@@ -13,6 +9,47 @@ export function setSafeRegex(validator) {
13
9
  safeRegex = validator;
14
10
  }
15
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
+
16
53
  /**
17
54
  * Récupère une valeur imbriquée dans un objet en utilisant une chaîne de chemin.
18
55
  * Gère les tableaux et les objets. Retourne undefined si le chemin n'est pas trouvé.
@@ -67,6 +104,17 @@ const evaluateSingleCondition = (currentModelDef, condition, formData, allModels
67
104
  return true; // Permissive default
68
105
  }
69
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
+
70
118
  // Si la condition est de la forme {field: value}, on la transforme en {$eq: value}
71
119
  if (!Object.keys(condition)[0].startsWith('$') && typeof condition[Object.keys(condition)[0]] !== 'object') {
72
120
  const fieldName = Object.keys(condition)[0];
@@ -81,12 +129,12 @@ const evaluateSingleCondition = (currentModelDef, condition, formData, allModels
81
129
  const [fieldPath, expectedValue] = condition[operator];
82
130
  if (typeof fieldPath === 'string' && fieldPath.startsWith('$')) {
83
131
  const fieldName = fieldPath.substring(1); // Enlève le $ devant
84
- const actualValue = formData[fieldName];
85
- if (operator === '$eq') return actualValue == expectedValue;
86
- if (operator === '$ne') return actualValue != expectedValue;
132
+ const actualValue = getNestedValue(formData, fieldName);
133
+ return evaluateComparison(operator, actualValue, expectedValue, condition, checkRegex);
87
134
  }
88
135
  }
89
136
 
137
+
90
138
  // Si la condition contient des opérateurs logiques, on les gère ici
91
139
  if (condition.$and || condition.$or || condition.$not || condition.$nor) {
92
140
  console.warn("[Client Eval] Condition logique détectée dans evaluateSingleCondition, ce n'est pas attendu. Devrait être géré par isConditionMet.");
@@ -165,7 +213,7 @@ const evaluateSingleCondition = (currentModelDef, condition, formData, allModels
165
213
  return false;
166
214
  }
167
215
 
168
- return evaluateComparison(fieldValue, targetValue, processedConditionValue, condition);
216
+ return evaluateComparison('$eq', targetValue, processedConditionValue, condition);
169
217
 
170
218
  function logClientEvalWarning(message, details) {
171
219
  console.warn(`[Client Eval] ${message}:`, details);
@@ -189,43 +237,6 @@ const evaluateSingleCondition = (currentModelDef, condition, formData, allModels
189
237
  }
190
238
  }
191
239
 
192
- function evaluateComparison(operator, targetValue, processedConditionValue, condition) {
193
- try {
194
- switch (typeof operator === 'object' ? Object.keys(operator)[0] : null) {
195
- case '$eq': return targetValue === processedConditionValue;
196
- case '$ne': return targetValue !== processedConditionValue;
197
- case '$gt': return targetValue > processedConditionValue;
198
- case '$lt': return targetValue < processedConditionValue;
199
- case '$gte': return targetValue >= processedConditionValue;
200
- case '$lte': return targetValue <= processedConditionValue;
201
- case '$regex':
202
- if (typeof targetValue !== 'string') return false;
203
- if (typeof processedConditionValue !== 'string') return false;
204
- try {
205
- // On vérifie si la fonction a été injectée ET si la regex est sûre.
206
- // Côté client, `safeRegex` sera null, donc la vérification est simplement ignorée.
207
- if( checkRegex && safeRegex && safeRegex(processedConditionValue)) {
208
- const regex = new RegExp(processedConditionValue, 'i');
209
- return regex.test(targetValue);
210
- }
211
- return false;
212
- } catch (e) {
213
- logClientEvalWarning(`Invalid regex pattern: ${processedConditionValue}`, condition);
214
- return false;
215
- }
216
- case '$in':
217
- return Array.isArray(processedConditionValue) && processedConditionValue.includes(String(targetValue));
218
- case '$nin':
219
- return !Array.isArray(processedConditionValue) || !processedConditionValue.includes(String(targetValue));
220
- default:
221
- logClientEvalWarning(`Unhandled operator in client evaluation logic: ${operator}`, condition);
222
- return true; // Permissive default
223
- }
224
- } catch (evalError) {
225
- logClientEvalWarning(`Error during client condition evaluation: ${operator}, targetValue=${targetValue}, processedConditionValue=${processedConditionValue}`, condition);
226
- return false;
227
- }
228
- }
229
240
  };
230
241
 
231
242
  export const isConditionMet = (model, cond, formData, allModels, user,checkRegex=true) => {
@@ -237,6 +248,22 @@ export const isConditionMet = (model, cond, formData, allModels, user,checkRegex
237
248
  return true;
238
249
  }
239
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
+
240
267
  // Cas spécial: Évaluation sans modèle (ex: pour les webhooks où la condition est déjà résolue)
241
268
  // Dans ce mode, la condition doit être un objet avec un seul opérateur.
242
269
  if (!model) {
@@ -314,131 +341,3 @@ export const isConditionMet = (model, cond, formData, allModels, user,checkRegex
314
341
  return evaluateSingleCondition(model, condition, formData, allModels, user, checkRegex);
315
342
  };
316
343
 
317
-
318
-
319
- /**
320
- * Remplace les placeholders dans un template (string, object, array) par des valeurs du contextData.
321
- * Version améliorée avec support des chemins complexes via resolvePathValue.
322
- */
323
- export async function substituteVariables(template, contextData, user) {
324
- // 1. Retourner les types non substituables tels quels
325
- if (template === null || (typeof template !== 'string' && typeof template !== 'object')) {
326
- return template;
327
- }
328
-
329
- // 2. Gérer les tableaux de manière récursive
330
- if (Array.isArray(template)) {
331
- return Promise.all(template.map(item => substituteVariables(item, contextData, user)));
332
- }
333
-
334
- // 3. Gérer les objets de manière récursive
335
- if (typeof template === 'object') {
336
- const newObj = {};
337
- for (const key in template) {
338
- if (Object.prototype.hasOwnProperty.call(template, key)) {
339
- const val = await substituteVariables(template[key], contextData, user);
340
- safeAssignObject(newObj, key, val);
341
- }
342
- }
343
- return newObj;
344
- }
345
-
346
- // --- À partir d'ici, nous savons que `template` est une chaîne de caractères ---
347
-
348
- // 4. Construire le contexte complet pour la substitution
349
- const dbCollection = await getCollectionForUser(user);
350
- const userEnvVars = await dbCollection.find({ _model: 'env', _user: user.username }).toArray();
351
- const userEnv = userEnvVars.reduce((acc, v) => ({ ...acc, [v.name]: v.value }), {});
352
-
353
- // `contextToSearch` contient toutes les données disponibles à sa racine
354
- const contextToSearch = { ...contextData, env: userEnv };
355
-
356
- // 5. Logique de résolution de valeur améliorée avec resolvePathValue
357
- const findValue = async (key) => {
358
- let path = key.trim();
359
- if (path.startsWith('context.')) {
360
- path = path.substring('context.'.length);
361
- }
362
- if (path.endsWith('._id')) {
363
- const basePath = path.slice(0, -4);
364
- const value = await findValue(basePath);
365
- return value?._id?.toString(); // Convertit l'ObjectId en string
366
- }
367
-
368
- // Gérer les valeurs dynamiques spéciales
369
- if (path === 'now') {
370
- return new Date().toISOString();
371
- } else if (path === 'randomUUID') {
372
- return crypto.randomUUID();
373
- } else if( path === "baseUrl" ){
374
- return process.env.NODE_ENV === 'production' ? 'https://'+getHost()+'/' : 'http://localhost:/'+port;
375
- }
376
-
377
- // Détecter si le chemin est complexe (contient plus d'un point)
378
- if (path.split('.').length > 1) {
379
- try {
380
- // Essayer de résoudre le chemin avec resolvePathValue
381
- const [root, ...rest] = path.split('.');
382
- // On vérifie si la racine du chemin (ex: 'triggerData') existe dans notre contexte
383
- if (contextToSearch[root]) {
384
- const resolvedValue = await resolvePathValue(
385
- rest.join('.'),
386
- contextToSearch[root], // On passe le bon objet de départ (ex: l'objet triggerData)
387
- user
388
- );
389
- if (resolvedValue !== undefined) {
390
- return resolvedValue;
391
- }
392
- }
393
- } catch (error) {
394
- console.warn(`Erreur lors de la résolution du chemin "${path}":`, error.message);
395
- // On continue avec la méthode normale si la résolution échoue
396
- }
397
- }
398
-
399
- // Fallback: chercher le chemin dans l'objet de contexte normal
400
- return getNestedValue(contextToSearch, path);
401
- };
402
-
403
- // CAS A : La chaîne est un unique placeholder (ex: "{context.triggerData.product.price}")
404
- const singlePlaceholderMatch = template.match(/^\{([^}]+)\}$/);
405
- if (singlePlaceholderMatch) {
406
- const key = singlePlaceholderMatch[1];
407
- const value = await findValue(key);
408
-
409
- if (value === undefined) {
410
- return template; // Placeholder not found, return as is.
411
- }
412
-
413
- // If the resolved value is a string, it might contain more placeholders.
414
- // We recursively call substituteVariables on it, but only if it's different
415
- // from the original template to prevent infinite loops.
416
- if (typeof value === 'string' && value !== template) {
417
- return substituteVariables(value, contextData, user);
418
- }
419
-
420
- // For non-string values or if value is same as template, return the value.
421
- return value;
422
- }
423
-
424
- // CAS B : La chaîne contient plusieurs placeholders ou mix texte/variables
425
- const placeholderRegex = /\{([^}]+)\}/g;
426
- const placeholders = [...template.matchAll(placeholderRegex)];
427
-
428
- // Si aucun placeholder trouvé, retourner la chaîne telle quelle
429
- if (placeholders.length === 0) {
430
- return template;
431
- }
432
-
433
- // Remplacer chaque placeholder de manière asynchrone
434
- let result = template;
435
- for (const [match, key] of placeholders) {
436
- const value = await findValue(key);
437
- const replacement = value !== undefined
438
- ? (value === null ? 'null' : typeof value === 'object' ? JSON.stringify(value) : String(value))
439
- : match;
440
- result = result.replace(match, replacement);
441
- }
442
-
443
- return result;
444
- }
@@ -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);
1486
+
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
+ }
1463
1491
 
1464
- // --- Début de la logique de suppression ---
1492
+ const collection = await getCollectionForUser(user);
1465
1493
 
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
- });
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 = [];
1472
1497
 
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))]});
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 });
1476
1504
  }
1477
1505
 
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
- });
1483
-
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}`;
@@ -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