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/README.md +114 -1009
- package/client/package-lock.json +64 -50
- package/client/package.json +6 -0
- package/client/src/App.scss +29 -0
- package/client/src/_variables.scss +3 -0
- package/doc/automation-workflows.md +102 -0
- package/doc/core-concepts.md +33 -0
- package/doc/custom-api-endpoints.md +40 -0
- package/doc/dashboards-kpis-charts.md +49 -0
- package/doc/data-management.md +120 -0
- package/doc/data-models.md +75 -0
- package/doc/index.md +14 -0
- package/doc/roles-permissions.md +43 -0
- package/doc/users.md +30 -0
- package/package.json +7 -5
- package/src/client.js +6 -4
- package/src/core.js +31 -12
- package/src/filter.js +72 -173
- package/src/modules/data/data.js +2 -1
- package/src/modules/data/data.operations.js +107 -69
- package/src/modules/data/data.relations.js +1 -1
- package/src/modules/mongodb.js +2 -1
- package/src/modules/swagger.js +25 -5
- package/src/modules/user.js +108 -78
- package/src/modules/workflow.js +138 -3
- package/swagger-en.yml +2308 -472
- package/swagger-fr.yml +487 -3
- package/test/core.test.js +341 -0
- package/test/data.integration.test.js +137 -2
- package/test/user.test.js +33 -29
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
|
|
85
|
-
|
|
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(
|
|
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
|
-
}
|
package/src/modules/data/data.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
await hasPermission(["API_ADD_DATA_NOT_" + modelName], user)
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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.
|
|
1100
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1168
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
1492
|
+
const collection = await getCollectionForUser(user);
|
|
1465
1493
|
|
|
1466
|
-
// 1. Construire le filtre de
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
-
//
|
|
1474
|
-
if (
|
|
1475
|
-
|
|
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
|
-
//
|
|
1479
|
-
if (
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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:
|
|
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.
|
|
537
|
+
docToProcess._user = me.username || me._user;
|
|
538
538
|
docToProcess._hash = getFieldValueHash(model, docToProcess);
|
|
539
539
|
|
|
540
540
|
return docToProcess;
|
package/src/modules/mongodb.js
CHANGED
|
@@ -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
|
|