data-primals-engine 1.2.2 → 1.2.3
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/client/src/DataEditor.jsx +1 -202
- package/client/src/DataTable.jsx +1 -1
- package/client/src/constants.js +1 -1
- package/package.json +1 -1
- package/src/constants.js +2 -2
- package/src/defaultModels.js +1 -14
- package/src/email.js +7 -5
- package/src/engine.js +2 -1
- package/src/events.js +1 -1
- package/src/filter.js +221 -0
- package/src/modules/data.js +9 -9
- package/src/modules/workflow.js +266 -100
- package/src/packs.js +245 -7
- package/test/data.backup.integration.test.js +4 -1
- package/test/data.integration.test.js +10 -4
- package/test/file.test.js +11 -14
- package/test/import_export.integration.test.js +23 -19
- package/test/model.integration.test.js +20 -19
- package/test/user.test.js +30 -23
- package/test/vm.test.js +51 -0
- package/test/workflow.integration.test.js +6 -1
- package/test/workflow.robustness.test.js +6 -1
|
@@ -25,6 +25,7 @@ import Draggable from "./Draggable.jsx";
|
|
|
25
25
|
import CronBuilder from "./CronBuilder.jsx";
|
|
26
26
|
import RTETrans from "./RTETrans.jsx";
|
|
27
27
|
import uniqid from "uniqid";
|
|
28
|
+
import {isConditionMet} from "data-primals-engine/filter";
|
|
28
29
|
|
|
29
30
|
// ... (fonction getInputType) ...
|
|
30
31
|
// Fonction pour obtenir le type d'input HTML basé sur le type de champ du modèle
|
|
@@ -52,208 +53,6 @@ const getInputType = (fieldType) => {
|
|
|
52
53
|
}
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
/**
|
|
56
|
-
* Evaluates a single condition against form data.
|
|
57
|
-
* @param {object} currentModelDef - The definition of the current model.
|
|
58
|
-
* @param {object} condition - The condition to evaluate.
|
|
59
|
-
* @param {object} formData - The form data.
|
|
60
|
-
* @param {object[]} allModels - An array of all model definitions.
|
|
61
|
-
* @param {object} user - The current user.
|
|
62
|
-
* @returns {boolean} - True if the condition is met, false otherwise.
|
|
63
|
-
*/
|
|
64
|
-
const evaluateSingleCondition = (currentModelDef, condition, formData, allModels, user) => {
|
|
65
|
-
// Condition est directement un filtre MongoDB, donc on l'applique
|
|
66
|
-
// en utilisant les opérateurs et les valeurs qu'il contient.
|
|
67
|
-
|
|
68
|
-
if (!condition || typeof condition !== 'object') {
|
|
69
|
-
console.warn("[Client Eval] Condition is not an object:", condition);
|
|
70
|
-
return true; // Permissive default
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Si la condition contient des opérateurs logiques, on les gère ici
|
|
74
|
-
if (condition.$and || condition.$or || condition.$not || condition.$nor) {
|
|
75
|
-
console.warn("[Client Eval] Condition logique détectée dans evaluateSingleCondition, ce n'est pas attendu. Devrait être géré par isConditionMet.");
|
|
76
|
-
return true; // Permissive default
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (condition.$find) {
|
|
80
|
-
const fieldName = Object.keys(condition)[0];
|
|
81
|
-
const fieldValue = formData[fieldName];
|
|
82
|
-
const findCondition = condition.$find;
|
|
83
|
-
|
|
84
|
-
if (!Array.isArray(fieldValue)) return false;
|
|
85
|
-
|
|
86
|
-
return fieldValue.some(item => {
|
|
87
|
-
// Gestion spéciale pour la syntaxe $eq: ["$$this.field", value]
|
|
88
|
-
if (findCondition.$eq && Array.isArray(findCondition.$eq)) {
|
|
89
|
-
const [fieldPath, value] = findCondition.$eq;
|
|
90
|
-
if (fieldPath.startsWith("$$this.")) {
|
|
91
|
-
const fieldToCheck = fieldPath.replace("$$this.", "");
|
|
92
|
-
return item[fieldToCheck] == value;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Sinon, évaluation normale
|
|
97
|
-
const tempData = { ...item };
|
|
98
|
-
return evaluateSingleCondition(currentModelDef, findCondition, tempData, allModels, user);
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Si la condition contient un opérateur $exists, on le gère ici
|
|
103
|
-
if (condition.$exists !== undefined) {
|
|
104
|
-
const fieldName = Object.keys(condition)[0]; // Récupérer le nom du champ
|
|
105
|
-
const shouldExist = condition.$exists; // Récupérer la valeur de $exists (true ou false)
|
|
106
|
-
const exists = Object.prototype.hasOwnProperty.call(formData, fieldName) && formData[fieldName] !== undefined && formData[fieldName] !== null;
|
|
107
|
-
return exists === shouldExist;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Si la condition contient un opérateur $find, on le gère ici
|
|
111
|
-
if (condition.$find) {
|
|
112
|
-
// Récupérer le nom du champ
|
|
113
|
-
const fieldName = Object.keys(condition)[0];
|
|
114
|
-
const fieldValue = formData[fieldName];
|
|
115
|
-
try {
|
|
116
|
-
// Assuming evaluateSingleCondition handles $find
|
|
117
|
-
return evaluateSingleCondition(currentModelDef, condition.$find, formData, allModels, user);
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.error("Error evaluating $find condition:", condition, error);
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Récupérer le nom du champ et la condition
|
|
125
|
-
const fieldName = Object.keys(condition)[0];
|
|
126
|
-
const fieldValue = condition[fieldName];
|
|
127
|
-
|
|
128
|
-
// Récupérer la définition du champ
|
|
129
|
-
const fieldDef = currentModelDef?.fields.find(f => f.name === fieldName);
|
|
130
|
-
|
|
131
|
-
// Si la définition du champ n'est pas trouvée, on retourne true
|
|
132
|
-
if (!fieldDef) {
|
|
133
|
-
console.warn(`[Client Eval] Field definition not found for field: ${fieldName}`);
|
|
134
|
-
return true; // Permissive default
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
let targetValue = formData[fieldName];
|
|
138
|
-
let processedConditionValue = fieldValue;
|
|
139
|
-
|
|
140
|
-
// 1. Handle $exists (on the first field)
|
|
141
|
-
// 2. Convert condition value based on operator's expected input type
|
|
142
|
-
const fieldType = fieldDef?.type; // Type of the first field
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
processedConditionValue = convertValueType(fieldValue, fieldType);
|
|
146
|
-
} catch (e) {
|
|
147
|
-
logClientEvalWarning(`Error converting value type: ${e.message}`, condition);
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return evaluateComparison(fieldValue, targetValue, processedConditionValue, condition);
|
|
152
|
-
|
|
153
|
-
function logClientEvalWarning(message, details) {
|
|
154
|
-
console.warn(`[Client Eval] ${message}:`, details);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function convertValueType(value, inputType) {
|
|
158
|
-
switch (inputType) {
|
|
159
|
-
case 'number':
|
|
160
|
-
const numValue = parseFloat(value);
|
|
161
|
-
if (isNaN(numValue)) {
|
|
162
|
-
throw new Error(`Invalid number value: ${value}`);
|
|
163
|
-
}
|
|
164
|
-
return numValue;
|
|
165
|
-
case 'boolean':
|
|
166
|
-
return String(value).toLowerCase() === 'true';
|
|
167
|
-
case 'csv':
|
|
168
|
-
return String(value).split(',').map(item => item.trim()).filter(Boolean);
|
|
169
|
-
case 'text':
|
|
170
|
-
default:
|
|
171
|
-
return String(value);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function evaluateComparison(operator, targetValue, processedConditionValue, condition) {
|
|
176
|
-
try {
|
|
177
|
-
switch (typeof operator === 'object' ? Object.keys(operator)[0] : null) {
|
|
178
|
-
case '$eq': return targetValue == processedConditionValue;
|
|
179
|
-
case '$ne': return targetValue != processedConditionValue;
|
|
180
|
-
case '$gt': return targetValue > processedConditionValue;
|
|
181
|
-
case '$lt': return targetValue < processedConditionValue;
|
|
182
|
-
case '$gte': return targetValue >= processedConditionValue;
|
|
183
|
-
case '$lte': return targetValue <= processedConditionValue;
|
|
184
|
-
case '$regex':
|
|
185
|
-
if (typeof targetValue !== 'string') return false;
|
|
186
|
-
if (typeof processedConditionValue !== 'string') return false;
|
|
187
|
-
try {
|
|
188
|
-
const regex = new RegExp(processedConditionValue, 'i');
|
|
189
|
-
return regex.test(targetValue);
|
|
190
|
-
} catch (e) {
|
|
191
|
-
logClientEvalWarning(`Invalid regex pattern: ${processedConditionValue}`, condition);
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
case '$in':
|
|
195
|
-
return Array.isArray(processedConditionValue) && processedConditionValue.includes(String(targetValue));
|
|
196
|
-
case '$nin':
|
|
197
|
-
return !Array.isArray(processedConditionValue) || !processedConditionValue.includes(String(targetValue));
|
|
198
|
-
default:
|
|
199
|
-
logClientEvalWarning(`Unhandled operator in client evaluation logic: ${operator}`, condition);
|
|
200
|
-
return true; // Permissive default
|
|
201
|
-
}
|
|
202
|
-
} catch (evalError) {
|
|
203
|
-
logClientEvalWarning(`Error during client condition evaluation: ${operator}, targetValue=${targetValue}, processedConditionValue=${processedConditionValue}`, condition);
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
export const isConditionMet = (model, cond, formData, allModels, user) => {
|
|
210
|
-
const isNode = (v) => typeof v === 'object' && v !== null;
|
|
211
|
-
|
|
212
|
-
const condition = cond;
|
|
213
|
-
|
|
214
|
-
if (!condition) return true;
|
|
215
|
-
|
|
216
|
-
if (condition.$and && Array.isArray(condition.$and)) {
|
|
217
|
-
if (condition.$and.length === 0) return true;
|
|
218
|
-
return condition.$and.every(sub => isConditionMet(model, sub, formData, allModels, user));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (condition.$or && Array.isArray(condition.$or)) {
|
|
222
|
-
if (condition.$or.length === 0) return false;
|
|
223
|
-
return condition.$or.some(sub => isConditionMet(model, sub, formData, allModels, user));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (condition.$not) {
|
|
227
|
-
return !isConditionMet(model, condition.$not, formData, allModels, user);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (condition.$nor && Array.isArray(condition.$nor)) {
|
|
231
|
-
if (condition.$nor.length === 0) return true;
|
|
232
|
-
return !condition.$nor.some(sub => isConditionMet(model, sub, formData, allModels, user));
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (condition.$find) {
|
|
236
|
-
try {
|
|
237
|
-
// Assuming evaluateSingleCondition handles $find
|
|
238
|
-
return evaluateSingleCondition(model, condition, formData, allModels, user);
|
|
239
|
-
} catch (error) {
|
|
240
|
-
console.error("Error evaluating $find condition:", condition, error);
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (condition.path && condition.op) {
|
|
246
|
-
try {
|
|
247
|
-
return evaluateSingleCondition(model, condition, formData, allModels, user);
|
|
248
|
-
} catch (error) {
|
|
249
|
-
console.error("Error evaluating condition:", condition, error);
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
console.warn("Unknown condition format:", condition);
|
|
255
|
-
return true;
|
|
256
|
-
};
|
|
257
56
|
|
|
258
57
|
export const DataEditor = forwardRef(function MyDataEditor({
|
|
259
58
|
isLoading,
|
package/client/src/DataTable.jsx
CHANGED
|
@@ -43,7 +43,6 @@ import RelationValue from "./RelationValue.jsx";
|
|
|
43
43
|
import {FaGear, FaPencil, FaTriangleExclamation} from "react-icons/fa6";
|
|
44
44
|
import RestoreConfirmationModal from "./RestoreConfirmationModal.jsx";
|
|
45
45
|
import {event_trigger, isLightColor} from "../../src/core.js";
|
|
46
|
-
import {isConditionMet} from "./DataEditor.jsx";
|
|
47
46
|
import {Tooltip} from "react-tooltip";
|
|
48
47
|
import ExportDialog from "./ExportDialog.jsx";
|
|
49
48
|
import Captions from "yet-another-react-lightbox/plugins/captions";
|
|
@@ -58,6 +57,7 @@ import PackGallery from "./PackGallery.jsx";
|
|
|
58
57
|
import {HiddenableCell} from "./HiddenableCell.jsx";
|
|
59
58
|
import ConditionBuilder from "./ConditionBuilder.jsx";
|
|
60
59
|
import {pagedFilterToMongoConds} from "./filter.js";
|
|
60
|
+
import {isConditionMet} from "data-primals-engine/filter";
|
|
61
61
|
|
|
62
62
|
// Ajoutez cette constante pour la clé de sessionStorage
|
|
63
63
|
const SESSION_STORAGE_IMPORT_JOBS_KEY = 'activeImportJobs';
|
package/client/src/constants.js
CHANGED
|
@@ -53,7 +53,7 @@ export const MONGO_OPERATORS = {
|
|
|
53
53
|
export const profiles = {
|
|
54
54
|
'personal': ['contact', 'location', 'imageGallery', 'budget', 'currency', 'taxonomy'], // budget,
|
|
55
55
|
'developer': ['alert','endpoint','request','webpage', 'content', 'taxonomy', 'resource', 'translation', 'contact', 'location', 'channel', 'lang', 'token', 'message', 'ticket', 'user', 'permission', 'role'],
|
|
56
|
-
'company': ['alert','request','location', '
|
|
56
|
+
'company': ['alert','request','location', 'order', 'currency', 'product', 'cart', 'cartItem', 'invoice', 'message', 'user', 'role', 'permission', 'token','translation', 'lang', 'webpage', 'content', 'taxonomy', 'contact', 'resource', 'accountingExercise', 'accountingLineItem', 'accountingEntry', 'employee', 'kpi', 'dashboard'],
|
|
57
57
|
'engineer': ['alert','endpoint','request','dashboard', 'kpi', 'user', 'role', 'token', 'permission', 'workflow', 'workflowRun', 'workflowStep', "channel", "message", 'workflowAction', 'workflowTrigger']
|
|
58
58
|
}
|
|
59
59
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "data-primals-engine is a package responsible from handling large amount of data using MongoDB in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation. It also has integrated AI assistant.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"type": "module",
|
package/src/constants.js
CHANGED
|
@@ -66,7 +66,7 @@ export const awsDefaultConfig = {
|
|
|
66
66
|
export const emailDefaultConfig = {
|
|
67
67
|
from: "Support - data@primals.net <data@primals.net>",
|
|
68
68
|
host: 'smtp.mydomain.tld',
|
|
69
|
-
port:
|
|
69
|
+
port: 587,
|
|
70
70
|
secure: false,
|
|
71
71
|
user: 'user',
|
|
72
72
|
pass: 'password'
|
|
@@ -288,7 +288,7 @@ metaModels['messaging'] = { load: ['alert','ticket', 'message', 'channel'], 'req
|
|
|
288
288
|
metaModels['eshopping'] = { load: [
|
|
289
289
|
'order', 'currency', 'product', 'productVariant', 'discount', 'cart', 'cartItem',
|
|
290
290
|
'brand', 'return', 'review', 'stock', 'returnItem', 'userSubscription',
|
|
291
|
-
'warehouse', 'shipment', '
|
|
291
|
+
'warehouse', 'shipment', 'stockAlert', 'invoice'],
|
|
292
292
|
'require': ['i18n', 'users', 'messaging'] };
|
|
293
293
|
metaModels['workflow'] = { load: ['env', 'workflow', 'workflowRun', 'workflowAction', 'workflowStep', 'workflowTrigger']};
|
|
294
294
|
metaModels['erp'] = { load: [ 'accountingExercise', 'accountingLineItem', 'accountingEntry', 'employee', 'dashboard', 'kpi'] };
|
package/src/defaultModels.js
CHANGED
|
@@ -543,19 +543,6 @@ export const defaultModels = {
|
|
|
543
543
|
{ name: 'createdAt', type: 'datetime', required: true }
|
|
544
544
|
]
|
|
545
545
|
},
|
|
546
|
-
campaign: {
|
|
547
|
-
name: 'campaign',
|
|
548
|
-
"description": "",
|
|
549
|
-
fields: [
|
|
550
|
-
{ name: 'name', type: 'string_t', required: true },
|
|
551
|
-
{ name: 'description', type: 'richtext' },
|
|
552
|
-
{ name: 'startDate', type: 'datetime' },
|
|
553
|
-
{ name: 'endDate', type: 'datetime' },
|
|
554
|
-
{ name: 'type', type: 'enum', items: ['email', 'sms', 'advertisement', 'promotion'] },
|
|
555
|
-
{ name: 'status', type: 'enum', items: ['planified', 'in_progress', 'finished', 'cancelled'] },
|
|
556
|
-
{ name: 'budget', type: 'number' }
|
|
557
|
-
]
|
|
558
|
-
},
|
|
559
546
|
review: {
|
|
560
547
|
name: "review",
|
|
561
548
|
"description": "",
|
|
@@ -1014,7 +1001,7 @@ export const defaultModels = {
|
|
|
1014
1001
|
{ name: 'owner', type: 'relation', relation: 'user', required: false },
|
|
1015
1002
|
{ name: 'startedAt', type: 'datetime', required: true, hint: "Timestamp when the workflow run began." },
|
|
1016
1003
|
{ name: 'completedAt', type: 'datetime', hint: "Timestamp when the workflow run finished (successfully or failed)." },
|
|
1017
|
-
{ name: '
|
|
1004
|
+
{ name: 'log', type: 'string', maxlength: 4096, hint: "Error message if the workflow run failed." }
|
|
1018
1005
|
]
|
|
1019
1006
|
},
|
|
1020
1007
|
dashboard:{
|
package/src/email.js
CHANGED
|
@@ -6,12 +6,12 @@ import {emailDefaultConfig} from "./constants.js";
|
|
|
6
6
|
|
|
7
7
|
// Le transporteur par défaut, utilisé si aucune config spécifique n'est fournie.
|
|
8
8
|
const defaultTransporter = nodemailer.createTransport({
|
|
9
|
-
host:
|
|
10
|
-
port: 587,
|
|
9
|
+
host: process.env.SMTP_HOST,
|
|
10
|
+
port: process.env.SMTP_PORT || 587,
|
|
11
11
|
secure: false,
|
|
12
12
|
auth: {
|
|
13
|
-
user: process.env.
|
|
14
|
-
pass: process.env.
|
|
13
|
+
user: process.env.SMTP_USER,
|
|
14
|
+
pass: process.env.SMTP_PASS
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
17
|
|
|
@@ -52,7 +52,9 @@ export const sendEmail = async (email = "", data, smtpConfig = null, lang, tpl =
|
|
|
52
52
|
// Choisir le transporteur à utiliser
|
|
53
53
|
const transporter = smtpConfig ? createTransporter(smtpConfig||emailDefaultConfig) : defaultTransporter;
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Event.Listen("OnEmailTemplate", (data, lang) => data.content, "event", "system");
|
|
56
|
+
|
|
57
|
+
if (tpl === null) tpl = Event.Trigger("OnEmailTemplate", "event", "system", data, lang);
|
|
56
58
|
let html = tpl;
|
|
57
59
|
try {
|
|
58
60
|
html = juice(tpl);
|
package/src/engine.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import http from "http";
|
|
14
14
|
import cookieParser from "cookie-parser";
|
|
15
15
|
import requestIp from 'request-ip';
|
|
16
|
-
import {createModel, deleteModels, getModels, validateModelStructure} from "./modules/data.js";
|
|
16
|
+
import {createModel, deleteModels, getModels, installAllPacks, validateModelStructure} from "./modules/data.js";
|
|
17
17
|
import {defaultModels} from "./defaultModels.js";
|
|
18
18
|
import {DefaultUserProvider} from "./providers.js";
|
|
19
19
|
import formidableMiddleware from 'express-formidable';
|
|
@@ -175,6 +175,7 @@ export const Engine = {
|
|
|
175
175
|
server.listen(port);
|
|
176
176
|
|
|
177
177
|
await setupInitialModels();
|
|
178
|
+
await installAllPacks();
|
|
178
179
|
|
|
179
180
|
if (cb)
|
|
180
181
|
await cb();
|
package/src/events.js
CHANGED
|
@@ -60,7 +60,7 @@ export const Event = {
|
|
|
60
60
|
}
|
|
61
61
|
return ret;
|
|
62
62
|
},
|
|
63
|
-
Listen: (name = "", callback
|
|
63
|
+
Listen: (name = "", callback, system = "priority", layer = "medium") => {
|
|
64
64
|
const validSystems = Object.keys(eventLayerSystems); // Récupération des clés pour une vérification plus performante
|
|
65
65
|
if (!validSystems.includes(system)) {
|
|
66
66
|
throw new Error(`System '${system}' does not exist. Valid systems are: ${validSystems.join(', ')}`); // Message d'erreur plus informatif
|
package/src/filter.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluates a single condition against form data.
|
|
3
|
+
* @param {object} currentModelDef - The definition of the current model.
|
|
4
|
+
* @param {object} condition - The condition to evaluate.
|
|
5
|
+
* @param {object} formData - The form data.
|
|
6
|
+
* @param {object[]} allModels - An array of all model definitions.
|
|
7
|
+
* @param {object} user - The current user.
|
|
8
|
+
* @returns {boolean} - True if the condition is met, false otherwise.
|
|
9
|
+
*/
|
|
10
|
+
const evaluateSingleCondition = (currentModelDef, condition, formData, allModels, user) => {
|
|
11
|
+
// Condition est directement un filtre MongoDB, donc on l'applique
|
|
12
|
+
// en utilisant les opérateurs et les valeurs qu'il contient.
|
|
13
|
+
|
|
14
|
+
if (!condition || typeof condition !== 'object') {
|
|
15
|
+
console.warn("[Client Eval] Condition is not an object:", condition);
|
|
16
|
+
return true; // Permissive default
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Si la condition est de la forme {field: value}, on la transforme en {$eq: value}
|
|
20
|
+
if (!Object.keys(condition)[0].startsWith('$') && typeof condition[Object.keys(condition)[0]] !== 'object') {
|
|
21
|
+
const fieldName = Object.keys(condition)[0];
|
|
22
|
+
const value = condition[fieldName];
|
|
23
|
+
return evaluateSingleCondition(currentModelDef, {[fieldName]: {$eq: value}}, formData, allModels, user);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Gestion des opérateurs d'agrégation (commencent par $)
|
|
27
|
+
const operator = Object.keys(condition)[0];
|
|
28
|
+
if (operator === '$eq' && Array.isArray(condition[operator])) {
|
|
29
|
+
// Cas spécial pour {$eq: ['$field', value]}
|
|
30
|
+
const [fieldPath, expectedValue] = condition[operator];
|
|
31
|
+
if (typeof fieldPath === 'string' && fieldPath.startsWith('$')) {
|
|
32
|
+
const fieldName = fieldPath.substring(1); // Enlève le $ devant
|
|
33
|
+
return formData[fieldName] == expectedValue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Si la condition contient des opérateurs logiques, on les gère ici
|
|
38
|
+
if (condition.$and || condition.$or || condition.$not || condition.$nor) {
|
|
39
|
+
console.warn("[Client Eval] Condition logique détectée dans evaluateSingleCondition, ce n'est pas attendu. Devrait être géré par isConditionMet.");
|
|
40
|
+
return true; // Permissive default
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (condition.$find) {
|
|
44
|
+
const fieldName = Object.keys(condition)[0];
|
|
45
|
+
const fieldValue = formData[fieldName];
|
|
46
|
+
const findCondition = condition.$find;
|
|
47
|
+
|
|
48
|
+
if (!Array.isArray(fieldValue)) return false;
|
|
49
|
+
|
|
50
|
+
return fieldValue.some(item => {
|
|
51
|
+
// Gestion spéciale pour la syntaxe $eq: ["$$this.field", value]
|
|
52
|
+
if (findCondition.$eq && Array.isArray(findCondition.$eq)) {
|
|
53
|
+
const [fieldPath, value] = findCondition.$eq;
|
|
54
|
+
if (fieldPath.startsWith("$$this.")) {
|
|
55
|
+
const fieldToCheck = fieldPath.replace("$$this.", "");
|
|
56
|
+
return item[fieldToCheck] == value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Sinon, évaluation normale
|
|
61
|
+
const tempData = { ...item };
|
|
62
|
+
return evaluateSingleCondition(currentModelDef, findCondition, tempData, allModels, user);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Si la condition contient un opérateur $exists, on le gère ici
|
|
67
|
+
if (condition.$exists !== undefined) {
|
|
68
|
+
const fieldName = Object.keys(condition)[0]; // Récupérer le nom du champ
|
|
69
|
+
const shouldExist = condition.$exists; // Récupérer la valeur de $exists (true ou false)
|
|
70
|
+
const exists = Object.prototype.hasOwnProperty.call(formData, fieldName) && formData[fieldName] !== undefined && formData[fieldName] !== null;
|
|
71
|
+
return exists === shouldExist;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Si la condition contient un opérateur $find, on le gère ici
|
|
75
|
+
if (condition.$find) {
|
|
76
|
+
// Récupérer le nom du champ
|
|
77
|
+
const fieldName = Object.keys(condition)[0];
|
|
78
|
+
const fieldValue = formData[fieldName];
|
|
79
|
+
try {
|
|
80
|
+
// Assuming evaluateSingleCondition handles $find
|
|
81
|
+
return evaluateSingleCondition(currentModelDef, condition.$find, formData, allModels, user);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("Error evaluating $find condition:", condition, error);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Récupérer le nom du champ et la condition
|
|
89
|
+
const fieldName = Object.keys(condition)[0];
|
|
90
|
+
const fieldValue = condition[fieldName];
|
|
91
|
+
|
|
92
|
+
// Récupérer la définition du champ
|
|
93
|
+
const fieldDef = currentModelDef?.fields.find(f => f.name === fieldName);
|
|
94
|
+
|
|
95
|
+
// Si la définition du champ n'est pas trouvée, on retourne true
|
|
96
|
+
if (!fieldDef) {
|
|
97
|
+
console.warn(`[Client Eval] Field definition not found for field: ${fieldName}`);
|
|
98
|
+
return true; // Permissive default
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let targetValue = formData[fieldName];
|
|
102
|
+
let processedConditionValue = fieldValue;
|
|
103
|
+
|
|
104
|
+
// 1. Handle $exists (on the first field)
|
|
105
|
+
// 2. Convert condition value based on operator's expected input type
|
|
106
|
+
const fieldType = fieldDef?.type; // Type of the first field
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
processedConditionValue = convertValueType(fieldValue, fieldType);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
logClientEvalWarning(`Error converting value type: ${e.message}`, condition);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return evaluateComparison(fieldValue, targetValue, processedConditionValue, condition);
|
|
116
|
+
|
|
117
|
+
function logClientEvalWarning(message, details) {
|
|
118
|
+
console.warn(`[Client Eval] ${message}:`, details);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function convertValueType(value, inputType) {
|
|
122
|
+
switch (inputType) {
|
|
123
|
+
case 'number':
|
|
124
|
+
const numValue = parseFloat(value);
|
|
125
|
+
if (isNaN(numValue)) {
|
|
126
|
+
throw new Error(`Invalid number value: ${value}`);
|
|
127
|
+
}
|
|
128
|
+
return numValue;
|
|
129
|
+
case 'boolean':
|
|
130
|
+
return String(value).toLowerCase() === 'true';
|
|
131
|
+
case 'csv':
|
|
132
|
+
return String(value).split(',').map(item => item.trim()).filter(Boolean);
|
|
133
|
+
case 'text':
|
|
134
|
+
default:
|
|
135
|
+
return String(value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function evaluateComparison(operator, targetValue, processedConditionValue, condition) {
|
|
140
|
+
try {
|
|
141
|
+
switch (typeof operator === 'object' ? Object.keys(operator)[0] : null) {
|
|
142
|
+
case '$eq': return targetValue == processedConditionValue;
|
|
143
|
+
case '$ne': return targetValue != processedConditionValue;
|
|
144
|
+
case '$gt': return targetValue > processedConditionValue;
|
|
145
|
+
case '$lt': return targetValue < processedConditionValue;
|
|
146
|
+
case '$gte': return targetValue >= processedConditionValue;
|
|
147
|
+
case '$lte': return targetValue <= processedConditionValue;
|
|
148
|
+
case '$regex':
|
|
149
|
+
if (typeof targetValue !== 'string') return false;
|
|
150
|
+
if (typeof processedConditionValue !== 'string') return false;
|
|
151
|
+
try {
|
|
152
|
+
const regex = new RegExp(processedConditionValue, 'i');
|
|
153
|
+
return regex.test(targetValue);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
logClientEvalWarning(`Invalid regex pattern: ${processedConditionValue}`, condition);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
case '$in':
|
|
159
|
+
return Array.isArray(processedConditionValue) && processedConditionValue.includes(String(targetValue));
|
|
160
|
+
case '$nin':
|
|
161
|
+
return !Array.isArray(processedConditionValue) || !processedConditionValue.includes(String(targetValue));
|
|
162
|
+
default:
|
|
163
|
+
logClientEvalWarning(`Unhandled operator in client evaluation logic: ${operator}`, condition);
|
|
164
|
+
return true; // Permissive default
|
|
165
|
+
}
|
|
166
|
+
} catch (evalError) {
|
|
167
|
+
logClientEvalWarning(`Error during client condition evaluation: ${operator}, targetValue=${targetValue}, processedConditionValue=${processedConditionValue}`, condition);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const isConditionMet = (model, cond, formData, allModels, user) => {
|
|
174
|
+
const condition = cond;
|
|
175
|
+
|
|
176
|
+
if (!condition) return true;
|
|
177
|
+
|
|
178
|
+
// Cas 1: Condition simple {field: value} → transformée en {field: {$eq: value}}
|
|
179
|
+
if (typeof condition === 'object' && !Array.isArray(condition)) {
|
|
180
|
+
const keys = Object.keys(condition);
|
|
181
|
+
if (keys.length === 1 && !keys[0].startsWith('$') &&
|
|
182
|
+
typeof condition[keys[0]] !== 'object') {
|
|
183
|
+
const simpleCondition = {
|
|
184
|
+
[keys[0]]: {$eq: condition[keys[0]]}
|
|
185
|
+
};
|
|
186
|
+
return evaluateSingleCondition(model, simpleCondition, formData, allModels, user);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Cas 2: Opérateurs logiques ($and, $or, $not, $nor)
|
|
191
|
+
if (condition.$and && Array.isArray(condition.$and)) {
|
|
192
|
+
if (condition.$and.length === 0) return true;
|
|
193
|
+
return condition.$and.every(sub => isConditionMet(model, sub, formData, allModels, user));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (condition.$or && Array.isArray(condition.$or)) {
|
|
197
|
+
if (condition.$or.length === 0) return false;
|
|
198
|
+
return condition.$or.some(sub => isConditionMet(model, sub, formData, allModels, user));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (condition.$not) {
|
|
202
|
+
return !isConditionMet(model, condition.$not, formData, allModels, user);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (condition.$nor && Array.isArray(condition.$nor)) {
|
|
206
|
+
if (condition.$nor.length === 0) return true;
|
|
207
|
+
return !condition.$nor.some(sub => isConditionMet(model, sub, formData, allModels, user));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Cas 3: Syntaxe d'agrégation {$eq: ['$field', value]}
|
|
211
|
+
if (Object.keys(condition).length === 1) {
|
|
212
|
+
const operator = Object.keys(condition)[0];
|
|
213
|
+
if (operator.startsWith('$') && operator !== '$find' &&
|
|
214
|
+
Array.isArray(condition[operator])) {
|
|
215
|
+
return evaluateSingleCondition(model, condition, formData, allModels, user);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Cas 4: Tous les autres cas (conditions normales avec opérateurs)
|
|
220
|
+
return evaluateSingleCondition(model, condition, formData, allModels, user);
|
|
221
|
+
};
|
package/src/modules/data.js
CHANGED
|
@@ -47,14 +47,14 @@ import {
|
|
|
47
47
|
} from "./mongodb.js";
|
|
48
48
|
import {dbUrl, MongoClient, MongoDatabase} from "../engine.js";
|
|
49
49
|
import path from "node:path";
|
|
50
|
-
import {getFileExtension, getObjectHash, getRandom, isGUID, isPlainObject, randomDate, uuidv4} from "../core.js";
|
|
50
|
+
import {getFileExtension, getObjectHash, getRandom, isGUID, isPlainObject, randomDate, sleep, uuidv4} from "../core.js";
|
|
51
51
|
import {Event} from "../events.js";
|
|
52
52
|
import fs from "node:fs";
|
|
53
53
|
import schedule from "node-schedule";
|
|
54
54
|
import {middleware} from "../middlewares/middleware-mongodb.js";
|
|
55
55
|
import i18n from "../i18n.js";
|
|
56
56
|
import {
|
|
57
|
-
executeSafeJavascript,
|
|
57
|
+
executeSafeJavascript, processWorkflowRun,
|
|
58
58
|
runScheduledJobWithDbLock,
|
|
59
59
|
scheduleWorkflowTriggers,
|
|
60
60
|
triggerWorkflows
|
|
@@ -94,7 +94,6 @@ const sseConnections = new Map();
|
|
|
94
94
|
const delay = ms => new Promise(res => setTimeout(res, ms));
|
|
95
95
|
|
|
96
96
|
const getBackupDir = () => process.env.BACKUP_DIR || './backups'; // Répertoire de stockage des sauvegardes
|
|
97
|
-
const execAsync = promisify(exec);
|
|
98
97
|
const execFileAsync = promisify(execFile);
|
|
99
98
|
|
|
100
99
|
let importJobs = {};
|
|
@@ -1386,6 +1385,7 @@ export async function onInit(defaultEngine) {
|
|
|
1386
1385
|
schedule.scheduleJob("0 2 * * *", jobDumpUserData);
|
|
1387
1386
|
//await jobDumpUserData();
|
|
1388
1387
|
|
|
1388
|
+
|
|
1389
1389
|
schedule.scheduleJob("0 0 * * *", async () => {
|
|
1390
1390
|
const dt = new Date();
|
|
1391
1391
|
dt.setTime(dt.getTime()-1000*3600*24*14);
|
|
@@ -1967,7 +1967,7 @@ export async function onInit(defaultEngine) {
|
|
|
1967
1967
|
const filter = req.fields.filter;
|
|
1968
1968
|
const hash = req.params.id; // Récupérer l'identifiant de la ressource à modifier
|
|
1969
1969
|
const data = req.fields.data || (req.fields._data && JSON.parse(req.fields._data));
|
|
1970
|
-
const r = await editData(req.fields.model, filter || hash, data, req.files, req.me)
|
|
1970
|
+
const r = await editData(req.fields.model, filter || { "$eq": ["$_id", { "$toObjectId": hash}]}, data, req.files, req.me)
|
|
1971
1971
|
if (r.error)
|
|
1972
1972
|
res.status(400).json(r);
|
|
1973
1973
|
else
|
|
@@ -2889,14 +2889,14 @@ export const deleteModels = async (filter) => {
|
|
|
2889
2889
|
}
|
|
2890
2890
|
|
|
2891
2891
|
export const getModel = async (modelName, user) => {
|
|
2892
|
-
const modelInCache = modelsCache.get(user
|
|
2892
|
+
const modelInCache = modelsCache.get((user?.username||'')+"@@"+modelName);
|
|
2893
2893
|
if(modelInCache)
|
|
2894
2894
|
return modelInCache;
|
|
2895
|
-
const model = await getCollection('models').findOne({name: modelName, $and: [{_user: {$exists: true}}, {$or: [{_user: user._user}, {_user: user.username}]}]});
|
|
2895
|
+
const model = await getCollection('models').findOne({name: modelName, $and: user ? [{_user: {$exists: true}}, {$or: [{_user: user._user}, {_user: user.username}]}] : [{_user: { $exists: false}}]});
|
|
2896
2896
|
if (!model) {
|
|
2897
2897
|
throw new Error(i18n.t('api.model.notFound', {model: modelName}));
|
|
2898
2898
|
}
|
|
2899
|
-
modelsCache.set(user
|
|
2899
|
+
modelsCache.set((user?.username||'')+"@@"+modelName, model);
|
|
2900
2900
|
return model;
|
|
2901
2901
|
}
|
|
2902
2902
|
export const getModels = async () => {
|
|
@@ -4135,7 +4135,7 @@ export const deleteData = async (modelName, filter, user ={}, triggerWorkflow, w
|
|
|
4135
4135
|
export const searchData = async (query, user) => {
|
|
4136
4136
|
const { page, limit, sort, model, ids, timeout, pack } = query; // Les filtres de la requête (attention aux injections MongoDB !)
|
|
4137
4137
|
|
|
4138
|
-
if( user.username !== 'demo' && isLocalUser(user) && (
|
|
4138
|
+
if( user && user.username !== 'demo' && isLocalUser(user) && (
|
|
4139
4139
|
!await hasPermission(["API_ADMIN", "API_SEARCH_DATA", "API_SEARCH_DATA_"+model], user) ||
|
|
4140
4140
|
await hasPermission(["API_SEARCH_DATA_NOT_"+model], user))){
|
|
4141
4141
|
throw new Error(i18n.t('api.permission.searchData'));
|
|
@@ -5988,4 +5988,4 @@ export async function handleDemoInitialization(req, res) {
|
|
|
5988
5988
|
logger.error(`[Demo Init] Critical error during initialization for user '${user.username}':`, error);
|
|
5989
5989
|
res.status(500).json({ success: false, error: 'An internal server error occurred during initialization.' });
|
|
5990
5990
|
}
|
|
5991
|
-
}
|
|
5991
|
+
}
|