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.
- package/README.md +56 -911
- package/client/package-lock.json +64 -50
- package/client/package.json +6 -0
- package/client/src/App.scss +29 -0
- package/client/src/Dashboard.jsx +1 -1
- package/client/src/WorkflowEditor.jsx +101 -0
- package/client/src/WorkflowEditor.scss +29 -4
- 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/defaultModels +1628 -0
- package/src/defaultModels.js +14 -0
- package/src/filter.js +110 -42
- package/src/modules/data/data.history.js +5 -1
- package/src/modules/data/data.js +2 -1
- package/src/modules/data/data.operations.js +116 -72
- 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 +138 -76
- package/src/modules/workflow.js +187 -177
- package/src/providers.js +1 -1
- package/swagger-en.yml +2308 -472
- package/swagger-fr.yml +487 -3
- package/test/core.test.js +341 -0
- package/test/data.history.integration.test.js +140 -16
- package/test/data.integration.test.js +137 -2
- package/test/user.test.js +201 -280
- package/test/workflow.integration.test.js +8 -0
package/src/defaultModels.js
CHANGED
|
@@ -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
|
|
45
|
-
|
|
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(
|
|
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
|
-
|
|
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 {
|
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);
|
|
1463
1486
|
|
|
1464
|
-
|
|
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
|
-
|
|
1467
|
-
let findFilter = [];
|
|
1468
|
-
if (user)
|
|
1469
|
-
findFilter.push({
|
|
1470
|
-
'$eq': ["$_user", user.username]
|
|
1471
|
-
});
|
|
1492
|
+
const collection = await getCollectionForUser(user);
|
|
1472
1493
|
|
|
1473
|
-
//
|
|
1474
|
-
|
|
1475
|
-
|
|
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
|
-
//
|
|
1479
|
-
if (
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
-
//
|
|
1485
|
-
if (filter &&
|
|
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}`;
|
|
@@ -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
|
-
|
|
1684
|
-
|
|
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.
|
|
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
|
|
package/src/modules/swagger.js
CHANGED
|
@@ -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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
};
|