data-primals-engine 1.5.0 → 1.5.1
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 +35 -0
- package/client/src/AddWidgetTypeModal.jsx +47 -43
- package/client/src/App.jsx +2 -6
- package/client/src/App.scss +12 -0
- package/client/src/AssistantChat.jsx +363 -323
- package/client/src/AssistantChat.scss +27 -10
- package/client/src/Dashboard.jsx +480 -396
- package/client/src/Dashboard.scss +1 -1
- package/client/src/DashboardHtmlViewItem.jsx +147 -0
- package/client/src/DashboardView.jsx +654 -569
- package/client/src/DataEditor.jsx +10 -3
- package/client/src/DataLayout.jsx +805 -755
- package/client/src/DataLayout.scss +14 -0
- package/client/src/DataTable.jsx +39 -75
- package/client/src/Dialog.scss +1 -1
- package/client/src/Field.jsx +2057 -1825
- package/client/src/FlexViewCard.jsx +44 -0
- package/client/src/HistoryDialog.jsx +47 -14
- package/client/src/HtmlViewBuilderModal.jsx +91 -0
- package/client/src/HtmlViewBuilderModal.scss +18 -0
- package/client/src/HtmlViewCard.jsx +44 -0
- package/client/src/HtmlViewCard.scss +35 -0
- package/client/src/KanbanCard.jsx +1 -2
- package/client/src/ModelCreator.jsx +5 -4
- package/client/src/ModelCreatorField.jsx +51 -4
- package/client/src/ModelList.jsx +92 -53
- package/client/src/Notification.jsx +136 -136
- package/client/src/Notification.scss +0 -18
- package/client/src/Pagination.jsx +5 -3
- package/client/src/RelationField.jsx +354 -258
- package/client/src/RelationSelectorWidget.jsx +173 -0
- package/client/src/contexts/ModelContext.jsx +10 -1
- package/client/src/contexts/UIContext.jsx +72 -63
- package/client/src/filter.js +262 -212
- package/client/src/hooks/useValidation.js +75 -0
- package/client/src/translations.js +24 -24
- package/package.json +2 -1
- package/src/constants.js +1 -1
- package/src/defaultModels.js +1596 -1544
- package/src/i18n.js +710 -10
- package/src/modules/assistant/assistant.js +148 -18
- package/src/modules/bucket.js +2 -1
- package/src/modules/data/data.core.js +118 -92
- package/src/modules/data/data.history.js +531 -492
- package/src/modules/data/data.js +3 -53
- package/src/modules/data/data.operations.js +77 -26
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +1879 -1821
- package/src/modules/data/data.validation.js +81 -2
- package/src/modules/file.js +247 -238
- package/src/packs.js +5482 -5478
- package/test/data.integration.test.js +1115 -1060
|
@@ -1,687 +1,687 @@
|
|
|
1
|
-
import {getDefaultForType, getFieldValueHash} from "../../data.js";
|
|
2
|
-
import {Event} from "../../events.js";
|
|
3
|
-
import {getCollectionForUser, isObjectId} from "../mongodb.js";
|
|
4
|
-
import {ObjectId} from "mongodb";
|
|
5
|
-
import {isPlainObject} from "../../core.js";
|
|
6
|
-
import {dataTypes, getModel, searchData} from "./data.operations.js";
|
|
7
|
-
import {validateModelData} from "./data.validation.js";
|
|
8
|
-
import i18n from "../../i18n.js";
|
|
9
|
-
import {addFile, removeFile} from "../file.js";
|
|
10
|
-
import {Logger} from "../../gameObject.js";
|
|
11
|
-
import NodeCache from "node-cache";
|
|
12
|
-
|
|
13
|
-
let depthFilter = 0;
|
|
14
|
-
|
|
15
|
-
// Création du cache avec des options configurables
|
|
16
|
-
export const relationCache = new NodeCache({
|
|
17
|
-
stdTTL: 3600, // TTL par défaut de 1 heure (en secondes)
|
|
18
|
-
checkperiod: 600, // Vérification des éléments expirés toutes les 10 minutes
|
|
19
|
-
useClones: false // Pour des performances optimales avec des ObjectId
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
let engine, logger;
|
|
23
|
-
export function onInit(defaultEngine) {
|
|
24
|
-
engine = defaultEngine;
|
|
25
|
-
logger = engine.getComponent(Logger);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function convertDataTypes(dataArray, modelFields, sourceType = 'csv') {
|
|
29
|
-
return dataArray.map(record => {
|
|
30
|
-
const convertedRecord = {...record};
|
|
31
|
-
for (const field of modelFields) {
|
|
32
|
-
if (convertedRecord.hasOwnProperty(field.name)) {
|
|
33
|
-
let value = convertedRecord[field.name];
|
|
34
|
-
|
|
35
|
-
// Gérer les chaînes vides pour les champs non requis
|
|
36
|
-
if (typeof value === 'string' && value === '' && !field.required) {
|
|
37
|
-
convertedRecord[field.name] = getDefaultForType(field);
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
// Si la valeur est null ou undefined, on la laisse telle quelle, la validation s'en chargera
|
|
41
|
-
if (value === null || value === undefined) {
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
switch (field.type) {
|
|
46
|
-
case 'number':
|
|
47
|
-
if (typeof value !== 'number') { // Convertir si ce n'est pas déjà un nombre
|
|
48
|
-
const num = parseFloat(value);
|
|
49
|
-
if (!isNaN(num)) {
|
|
50
|
-
convertedRecord[field.name] = num;
|
|
51
|
-
} else {
|
|
52
|
-
logger.warn(`Import: Impossible de parser le nombre pour le champ ${field.name}, valeur: ${value}. Utilisation de la valeur par défaut/null.`);
|
|
53
|
-
convertedRecord[field.name] = getDefaultForType(field);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
break;
|
|
57
|
-
case 'boolean':
|
|
58
|
-
if (typeof value !== 'boolean') {
|
|
59
|
-
convertedRecord[field.name] = ['true', '1', 'yes', 'on'].includes(String(value).toLowerCase());
|
|
60
|
-
}
|
|
61
|
-
break;
|
|
62
|
-
case 'date':
|
|
63
|
-
case 'datetime':
|
|
64
|
-
if (String(value).toLowerCase() === 'now') {
|
|
65
|
-
convertedRecord[field.name] = 'now';
|
|
66
|
-
} else {
|
|
67
|
-
const parsedDate = new Date(value);
|
|
68
|
-
if (!isNaN(parsedDate.getTime())) {
|
|
69
|
-
convertedRecord[field.name] = field.type === 'date' ? parsedDate.toISOString().split("T")[0] : parsedDate.toISOString();
|
|
70
|
-
} else if (value) { // Ne pas logger si la valeur était initialement vide/null
|
|
71
|
-
logger.warn(`Import: Impossible de parser la date pour le champ ${field.name}, valeur: ${value}. La validation ulture s'en chargera.`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
break;
|
|
75
|
-
case 'array':
|
|
76
|
-
if (['csv', 'excel'].includes(sourceType) && typeof value === 'string') {
|
|
77
|
-
const arrayValues = value.split(/[,;]/).map(item => item.trim()).filter(item => item !== '');
|
|
78
|
-
if (field.itemsType === 'number') {
|
|
79
|
-
convertedRecord[field.name] = arrayValues.map(v => parseFloat(v)).filter(v => !isNaN(v));
|
|
80
|
-
} else {
|
|
81
|
-
convertedRecord[field.name] = arrayValues;
|
|
82
|
-
}
|
|
83
|
-
} else if (sourceType === 'json' && typeof value === 'string') {
|
|
84
|
-
try {
|
|
85
|
-
const parsedArray = JSON.parse(value);
|
|
86
|
-
if (Array.isArray(parsedArray)) {
|
|
87
|
-
convertedRecord[field.name] = parsedArray;
|
|
88
|
-
// TODO: Potentiellement convertir les éléments de parsedArray ici si nécessaire
|
|
89
|
-
} else {
|
|
90
|
-
logger.warn(`Import: La chaîne JSON pour le champ tableau ${field.name} n'a pas été parsée en tableau. Valeur: ${value}.`);
|
|
91
|
-
}
|
|
92
|
-
} catch (e) {
|
|
93
|
-
logger.warn(`Import: Impossible de parser la chaîne JSON pour le champ tableau ${field.name}. Valeur: ${value}.`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Si c'est déjà un tableau (cas JSON typique), on suppose que les types des éléments sont corrects
|
|
97
|
-
// ou seront validés par pushDataUnsecure.
|
|
98
|
-
else if (!Array.isArray(convertedRecord[field.name])) {
|
|
99
|
-
convertedRecord[field.name] = getDefaultForType(field);
|
|
100
|
-
}
|
|
101
|
-
break;
|
|
102
|
-
case 'object':
|
|
103
|
-
if (['csv', 'excel'].includes(sourceType)) {
|
|
104
|
-
try {
|
|
105
|
-
convertedRecord[field.name] = JSON.parse(value);
|
|
106
|
-
} catch (e) {
|
|
107
|
-
logger.warn(`Import: Impossible de parser la chaîne JSON pour le champ objet ${field.name}. Valeur: ${value}.`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
break;
|
|
111
|
-
case 'code':
|
|
112
|
-
if (['csv', 'excel'].includes(sourceType) && typeof value === 'string') {
|
|
113
|
-
try {
|
|
114
|
-
convertedRecord[field.name] = JSON.parse(value);
|
|
115
|
-
} catch (e) {
|
|
116
|
-
logger.warn(`Import: Impossible de parser la chaîne JSON pour le champ code (json) ${field.name}. Valeur: ${value}.`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return convertedRecord;
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export const removeValue = (obj, containsKey, removeParent = false) => {
|
|
128
|
-
// Base case: If the object is not an object or array, return it as is.
|
|
129
|
-
if (!isPlainObject(obj) && !Array.isArray(obj)) {
|
|
130
|
-
return obj;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
for (const key in obj) {
|
|
134
|
-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
135
|
-
if (removeParent) {
|
|
136
|
-
const value = obj[key]?.[containsKey];
|
|
137
|
-
if (value !== undefined) {
|
|
138
|
-
delete obj[key];
|
|
139
|
-
} else {
|
|
140
|
-
removeValue(obj[key], containsKey);
|
|
141
|
-
}
|
|
142
|
-
} else if (containsKey === key) {
|
|
143
|
-
delete obj[key];
|
|
144
|
-
} else {
|
|
145
|
-
removeValue(obj[key], containsKey);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return obj;
|
|
150
|
-
};
|
|
151
|
-
export const changeValue = (obj, keyToChange, changeFunction, excludeKeys = [], depth = 0, parentKey = '') => {
|
|
152
|
-
if (!depth) {
|
|
153
|
-
depthFilter = 0;
|
|
154
|
-
}
|
|
155
|
-
if (!isPlainObject(obj) && !Array.isArray(obj)) {
|
|
156
|
-
return obj;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const newObj = Array.isArray(obj) ? [] : {};
|
|
160
|
-
|
|
161
|
-
for (const key in obj) {
|
|
162
|
-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
163
|
-
const topLevel = depthFilter === 0;
|
|
164
|
-
if (key === keyToChange) {
|
|
165
|
-
depthFilter++;
|
|
166
|
-
}
|
|
167
|
-
const value = obj[key];
|
|
168
|
-
if (value instanceof RegExp) {
|
|
169
|
-
newObj[key] = value;
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
if (isPlainObject(value) && !excludeKeys.includes(key)) {
|
|
173
|
-
newObj[key] = changeValue(value, keyToChange, changeFunction, excludeKeys, depth + 1, key);
|
|
174
|
-
} else if (Array.isArray(value) && !excludeKeys.includes(key)) {
|
|
175
|
-
newObj[key] = value.map(item => {
|
|
176
|
-
if (isPlainObject(item)) {
|
|
177
|
-
return changeValue(item, keyToChange, changeFunction, excludeKeys, depth + 1, key);
|
|
178
|
-
}
|
|
179
|
-
return item;
|
|
180
|
-
});
|
|
181
|
-
} else {
|
|
182
|
-
newObj[key] = value;
|
|
183
|
-
}
|
|
184
|
-
if (key === keyToChange) {
|
|
185
|
-
if (typeof changeFunction === 'function') {
|
|
186
|
-
const newValue = changeFunction(parentKey, newObj[key], topLevel);
|
|
187
|
-
if (newValue !== undefined) {
|
|
188
|
-
if (isPlainObject(newValue)) {
|
|
189
|
-
return newValue;
|
|
190
|
-
} else {
|
|
191
|
-
newObj[key] = newValue;
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
//delete newObj[key];
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return newObj;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
export async function processDocuments(datas, model, collection, me) {
|
|
205
|
-
const idMap = new Map();
|
|
206
|
-
const allInsertedIds = [];
|
|
207
|
-
|
|
208
|
-
const realData = await Event.Trigger("OnDataInsert", "event", "system", datas) || datas;
|
|
209
|
-
for (const doc of realData) {
|
|
210
|
-
try {
|
|
211
|
-
const newDocId = await insertAndResolveRelations(doc, model, collection, me, idMap);
|
|
212
|
-
if (newDocId) {
|
|
213
|
-
allInsertedIds.push(newDocId.toString());
|
|
214
|
-
}
|
|
215
|
-
} catch (error) {
|
|
216
|
-
// Modification clé ici : on ne catch plus les erreurs de validation
|
|
217
|
-
throw error;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return {allInsertedIds, idMap};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Traite toutes les relations du document
|
|
226
|
-
*/
|
|
227
|
-
export async function processRelations(docToProcess, model, collection, me, idMap) {
|
|
228
|
-
const batchFinds = [];
|
|
229
|
-
|
|
230
|
-
// Phase 1: Préparation des requêtes
|
|
231
|
-
for (const field of model.fields) {
|
|
232
|
-
if (field.type !== 'relation') continue;
|
|
233
|
-
|
|
234
|
-
const value = docToProcess[field.name];
|
|
235
|
-
if (value?.$find) {
|
|
236
|
-
batchFinds.push({
|
|
237
|
-
field: field.name,
|
|
238
|
-
promise: searchData({
|
|
239
|
-
filter: value.$find,
|
|
240
|
-
limit: field.multiple ? 0 : 1,
|
|
241
|
-
model: field.relation
|
|
242
|
-
}, me),
|
|
243
|
-
multiple: field.multiple
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Phase 2: Exécution parallèle
|
|
249
|
-
const findResults = await Promise.all(batchFinds.map(f => f.promise));
|
|
250
|
-
|
|
251
|
-
// Phase 3: Traitement des résultats
|
|
252
|
-
findResults.forEach((result, index) => {
|
|
253
|
-
const {field, multiple} = batchFinds[index];
|
|
254
|
-
if (result.data?.length > 0) {
|
|
255
|
-
// Cas où des documents sont trouvés
|
|
256
|
-
docToProcess[field] = multiple
|
|
257
|
-
? result.data.map(r => r._id.toString())
|
|
258
|
-
: result.data[0]._id.toString();
|
|
259
|
-
} else {
|
|
260
|
-
// Cas où AUCUN document n'est trouvé : il faut nettoyer le champ !
|
|
261
|
-
docToProcess[field] = multiple ? [] : null;
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
for (const field of model.fields) {
|
|
267
|
-
if (field.type !== 'relation') continue;
|
|
268
|
-
|
|
269
|
-
const fieldName = field.name;
|
|
270
|
-
const relationValue = docToProcess[fieldName];
|
|
271
|
-
if (!relationValue || typeof relationValue !== 'object') continue;
|
|
272
|
-
|
|
273
|
-
const relatedModel = await getModel(field.relation, me);
|
|
274
|
-
|
|
275
|
-
if (!Array.isArray(relationValue) && relationValue['$find']) {
|
|
276
|
-
|
|
277
|
-
} else if (Array.isArray(relationValue)) {
|
|
278
|
-
// Relation multiple (tableau)
|
|
279
|
-
docToProcess[fieldName] = await processMultipleRelations(
|
|
280
|
-
relationValue,
|
|
281
|
-
relatedModel,
|
|
282
|
-
collection,
|
|
283
|
-
me,
|
|
284
|
-
idMap
|
|
285
|
-
);
|
|
286
|
-
} else if (isPlainObject(relationValue)) {
|
|
287
|
-
// Relation simple (objet)
|
|
288
|
-
docToProcess[fieldName] = await processSingleRelation(
|
|
289
|
-
relationValue,
|
|
290
|
-
relatedModel,
|
|
291
|
-
collection,
|
|
292
|
-
me,
|
|
293
|
-
idMap
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Traite une relation multiple (tableau)
|
|
301
|
-
*/
|
|
302
|
-
async function processMultipleRelations(items, relatedModel, collection, me, idMap) {
|
|
303
|
-
const newRelationIds = await Promise.all(
|
|
304
|
-
items.map(item => processRelationItem(item, relatedModel, collection, me, idMap))
|
|
305
|
-
);
|
|
306
|
-
return newRelationIds.filter(id => id).map(id => id.toString());
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Traite une relation simple (objet)
|
|
311
|
-
*/
|
|
312
|
-
async function processSingleRelation(item, relatedModel, collection, me, idMap) {
|
|
313
|
-
const newId = await processRelationItem(item, relatedModel, collection, me, idMap);
|
|
314
|
-
return newId ? newId.toString() : null;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async function processRelationItem(item, relatedModel, collection, me, idMap) {
|
|
318
|
-
// Cas 1: ID existant (string ou ObjectId)
|
|
319
|
-
if (isObjectId(item) || typeof item === 'string') {
|
|
320
|
-
const originalId = typeof item === 'string' ? item : item.toString();
|
|
321
|
-
|
|
322
|
-
// Vérifier si cet ID a déjà été mappé (cas d'une référence circulaire)
|
|
323
|
-
if (idMap.has(originalId)) {
|
|
324
|
-
return idMap.get(originalId);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Sinon, vérifier si l'ID existe en base
|
|
328
|
-
const existing = await collection.findOne({
|
|
329
|
-
_id: new ObjectId(originalId),
|
|
330
|
-
_model: relatedModel.name,
|
|
331
|
-
$or: [{_user: me._user || me.username}, {_user: {$exists: false}}]
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
if (existing) {
|
|
335
|
-
return existing._id; // Conserver l'ID original
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Cas 2: Objet complet à importer
|
|
340
|
-
if (isPlainObject(item)) {
|
|
341
|
-
const relationDoc = prepareDocument(item, relatedModel, me);
|
|
342
|
-
applyDefaultValues(relationDoc, relatedModel);
|
|
343
|
-
|
|
344
|
-
// Si l'objet a un _id, essayer de le conserver
|
|
345
|
-
if (item._id) {
|
|
346
|
-
const originalId = item._id.toString();
|
|
347
|
-
|
|
348
|
-
// Vérifier si l'ID existe déjà en base
|
|
349
|
-
const existing = await collection.findOne({
|
|
350
|
-
_id: new ObjectId(originalId),
|
|
351
|
-
_model: relatedModel.name,
|
|
352
|
-
$or: [{_user: me._user || me.username}, {_user: {$exists: false}}]
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
if (existing) {
|
|
356
|
-
return existing._id; // Utiliser l'ID existant
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Si l'ID n'existe pas encore, l'utiliser pour le nouvel insert
|
|
360
|
-
relationDoc._id = new ObjectId(originalId);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const relationHash = relationDoc._hash;
|
|
364
|
-
const cacheKey = `${relatedModel.name}:${relationHash}`;
|
|
365
|
-
|
|
366
|
-
// Vérification dans le cache
|
|
367
|
-
const cachedId = relationCache.get(cacheKey);
|
|
368
|
-
if (cachedId !== undefined) {
|
|
369
|
-
return cachedId;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Vérification en base de données par hash
|
|
373
|
-
const existingByHash = await collection.findOne({
|
|
374
|
-
_hash: relationHash,
|
|
375
|
-
_model: relatedModel.name,
|
|
376
|
-
_user: relationDoc._user
|
|
377
|
-
}, {projection: {_id: 1}});
|
|
378
|
-
|
|
379
|
-
if (existingByHash) {
|
|
380
|
-
relationCache.set(cacheKey, existingByHash._id);
|
|
381
|
-
return existingByHash._id;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const newId = await insertAndResolveRelations(item, relatedModel, collection, me, idMap);
|
|
385
|
-
relationCache.set(cacheKey, newId);
|
|
386
|
-
return newId;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Fonction pour vider le cache si besoin
|
|
393
|
-
function clearRelationCache() {
|
|
394
|
-
relationCache.flushAll();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Applique les filtres de champ définis dans le modèle
|
|
399
|
-
*/
|
|
400
|
-
async function applyFieldFilters(docToProcess, model) {
|
|
401
|
-
for (const field of model.fields) {
|
|
402
|
-
docToProcess[field.name] = typeof (docToProcess[field.name]) === 'undefined' || docToProcess[field.name] === null ? field.default : docToProcess[field.name];
|
|
403
|
-
if (dataTypes[field.type]?.filter) {
|
|
404
|
-
const filter = await dataTypes[field.type].filter(
|
|
405
|
-
docToProcess[field.name],
|
|
406
|
-
field
|
|
407
|
-
);
|
|
408
|
-
const realFilter = await Event.Trigger('OnDataFilter', "event", "system", filter, field, docToProcess);
|
|
409
|
-
docToProcess[field.name] = realFilter || filter;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Applique les valeurs par défaut aux champs manquants
|
|
416
|
-
*/
|
|
417
|
-
function applyDefaultValues(doc, model) {
|
|
418
|
-
for (const field of model.fields) {
|
|
419
|
-
// Si le champ n'est pas défini et a une valeur par défaut
|
|
420
|
-
if (!(field.name in doc) && 'default' in field) {
|
|
421
|
-
doc[field.name] = typeof field.default === 'function'
|
|
422
|
-
? field.default()
|
|
423
|
-
: field.default;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async function insertAndResolveRelations(doc, model, collection, me, idMap) {
|
|
429
|
-
const originalId = doc._id?.toString();
|
|
430
|
-
|
|
431
|
-
// Si cet ID a déjà été traité, retourner le nouvel ID mappé
|
|
432
|
-
if (originalId && idMap.has(originalId)) {
|
|
433
|
-
return idMap.get(originalId);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const docToProcess = prepareDocument(doc, model, me);
|
|
437
|
-
applyDefaultValues(docToProcess, model);
|
|
438
|
-
|
|
439
|
-
// Si le document a un _id original et qu'il n'existe pas encore, le conserver
|
|
440
|
-
if (originalId && !await collection.findOne({_id: new ObjectId(originalId)})) {
|
|
441
|
-
docToProcess._id = new ObjectId(originalId);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
await validateModelData(docToProcess, model);
|
|
445
|
-
await processRelations(docToProcess, model, collection, me, idMap);
|
|
446
|
-
await validateModelData(docToProcess, model);
|
|
447
|
-
await applyFieldFilters(docToProcess, model);
|
|
448
|
-
await checkUniqueFields(docToProcess, model, collection);
|
|
449
|
-
|
|
450
|
-
const existingDoc = await findExistingDocument(docToProcess, collection);
|
|
451
|
-
if (existingDoc) {
|
|
452
|
-
cacheDocumentId(originalId, existingDoc._id, idMap);
|
|
453
|
-
return existingDoc._id;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
for (const field of model.fields) {
|
|
457
|
-
if (field.type === 'relation' && field.relationFilter && docToProcess[field.name]) {
|
|
458
|
-
|
|
459
|
-
const relatedIds = Array.isArray(docToProcess[field.name])
|
|
460
|
-
? docToProcess[field.name]
|
|
461
|
-
: [docToProcess[field.name]];
|
|
462
|
-
|
|
463
|
-
// Préparer un filtre global : match si _id dans relatedIds ET respecte relationFilter
|
|
464
|
-
const validationQuery = {
|
|
465
|
-
$and: [
|
|
466
|
-
{$in: ['$_id', relatedIds.map(id => ({$toObjectId: id}))]},
|
|
467
|
-
field.relationFilter
|
|
468
|
-
]
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
const relatedDocs = await searchData({
|
|
472
|
-
filter: validationQuery,
|
|
473
|
-
model: field.relation,
|
|
474
|
-
limit: relatedIds.length
|
|
475
|
-
}, me);
|
|
476
|
-
|
|
477
|
-
if ((relatedDocs?.count || 0) !== relatedIds.length) {
|
|
478
|
-
const invalidIds = relatedIds.filter(id =>
|
|
479
|
-
!relatedDocs.data.some(doc => doc._id.toString() === id.toString())
|
|
480
|
-
);
|
|
481
|
-
throw new Error(
|
|
482
|
-
i18n.t(
|
|
483
|
-
'api.data.relationFilterFailed',
|
|
484
|
-
'Les valeurs {{values}} pour le champ {{field}} ne respectent pas le filtre de relation défini.',
|
|
485
|
-
{field: field.name, values: invalidIds.join(', ')}
|
|
486
|
-
)
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
// Insertion en conservant éventuellement l'ID original
|
|
494
|
-
const result = docToProcess._id
|
|
495
|
-
? await collection.insertOne(docToProcess)
|
|
496
|
-
: await collection.insertOne(docToProcess);
|
|
497
|
-
|
|
498
|
-
const insertedId = result.insertedId;
|
|
499
|
-
cacheDocumentId(originalId, insertedId, idMap);
|
|
500
|
-
|
|
501
|
-
return insertedId;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Nouvelle fonction pour vérifier les champs uniques
|
|
505
|
-
async function checkUniqueFields(doc, model, collection) {
|
|
506
|
-
const uniqueFields = model.fields.filter(f => f.unique);
|
|
507
|
-
|
|
508
|
-
for (const field of uniqueFields) {
|
|
509
|
-
const value = doc[field.name];
|
|
510
|
-
if (value === undefined || value === null) continue;
|
|
511
|
-
|
|
512
|
-
const existing = await collection.findOne({
|
|
513
|
-
[field.name]: value,
|
|
514
|
-
_model: model.name,
|
|
515
|
-
_user: doc._user
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
if (existing) {
|
|
519
|
-
// Utilisation de i18n pour un message d'erreur standardisé
|
|
520
|
-
throw new Error(i18n.t('api.data.duplicateValue', {field: field.name, value: value}));
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function prepareDocument(doc, model, me) {
|
|
526
|
-
const docToProcess = {...doc};
|
|
527
|
-
delete docToProcess._id;
|
|
528
|
-
|
|
529
|
-
// AJOUT: Nettoyage des champs non définis dans le modèle
|
|
530
|
-
for (const key of Object.keys(docToProcess)) {
|
|
531
|
-
if (!model.fields.some(f => f.name === key) && !key.startsWith('_')) {
|
|
532
|
-
delete docToProcess[key];
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
docToProcess._model = model.name;
|
|
537
|
-
docToProcess._user = me._user || me.username;
|
|
538
|
-
docToProcess._hash = getFieldValueHash(model, docToProcess);
|
|
539
|
-
|
|
540
|
-
return docToProcess;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Cherche un document existant par son hash
|
|
545
|
-
*/
|
|
546
|
-
async function findExistingDocument(docToProcess, collection) {
|
|
547
|
-
return await collection.findOne({
|
|
548
|
-
_hash: docToProcess._hash,
|
|
549
|
-
_model: docToProcess._model,
|
|
550
|
-
_user: docToProcess._user
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* Insère le document dans la collection
|
|
556
|
-
*/
|
|
557
|
-
async function insertDocument(docToProcess, collection) {
|
|
558
|
-
const result = await collection.insertOne(docToProcess);
|
|
559
|
-
return result.insertedId;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Met en cache la correspondance d'ID
|
|
564
|
-
*/
|
|
565
|
-
function cacheDocumentId(originalId, newId, idMap) {
|
|
566
|
-
if (originalId && newId) {
|
|
567
|
-
idMap.set(originalId, newId);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Gestion des fichiers (à implémenter selon besoins)
|
|
573
|
-
*/
|
|
574
|
-
export async function handleFilesIfNeeded(insertedIds, files, model, collection) {
|
|
575
|
-
// Implémentation spécifique à votre application
|
|
576
|
-
// Ex: association des fichiers uploadés aux documents insérés
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
export const checkHash = async (me, model, hash, excludeId = null) => {
|
|
580
|
-
const collection = await getCollectionForUser(me);
|
|
581
|
-
const query = {
|
|
582
|
-
_model: model.name,
|
|
583
|
-
_hash: hash,
|
|
584
|
-
...(excludeId && {_id: {$ne: new ObjectId(excludeId)}})
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
console.log("Query being executed:", JSON.stringify(query, null, 2));
|
|
588
|
-
|
|
589
|
-
const count = await collection.countDocuments(query);
|
|
590
|
-
return count > 0;
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
// Fonctions helper
|
|
594
|
-
export async function processFileArray(files, currentFiles, user) {
|
|
595
|
-
const newFiles = await Promise.allSettled(
|
|
596
|
-
Object.keys(files).map(f => files[f]).map(async (file, i) => {
|
|
597
|
-
const oldFile = currentFiles.find(f => f.name === file.name);
|
|
598
|
-
if (oldFile && !file.newFile) return oldFile;
|
|
599
|
-
if (file.guid) return file;
|
|
600
|
-
if (!file.newFile) return Promise.reject();
|
|
601
|
-
return await addFile(files[i], user);
|
|
602
|
-
})
|
|
603
|
-
).then(results => results.map(r => r.value).filter(Boolean));
|
|
604
|
-
|
|
605
|
-
// Suppression des anciens fichiers non réutilisés
|
|
606
|
-
await Promise.allSettled(
|
|
607
|
-
currentFiles
|
|
608
|
-
.filter(f => !newFiles.some(nf => nf._id === f._id))
|
|
609
|
-
.map(f => removeFile(f, user))
|
|
610
|
-
);
|
|
611
|
-
|
|
612
|
-
return newFiles;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function handleCalculationExpression(calcExpression, fi, modelElement, calculationName) {
|
|
616
|
-
// Check if the calculation expression involves an operator
|
|
617
|
-
if (typeof calcExpression === 'object' && calcExpression !== null && Object.keys(calcExpression).length === 1) {
|
|
618
|
-
const operator = Object.keys(calcExpression)[0];
|
|
619
|
-
const operands = calcExpression[operator];
|
|
620
|
-
|
|
621
|
-
// Validation of isValidAggregationOperator
|
|
622
|
-
if (!isValidAggregationOperator(operator)) {
|
|
623
|
-
logger.warn(`Invalid aggregation operator '${operator}' in calculation. Skipping.`);
|
|
624
|
-
return null;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Check Operand Count and Apply $ifNull handling
|
|
628
|
-
if (Array.isArray(operands)) {
|
|
629
|
-
const handledOperands = operands.map(operand => handleOperand(operand, fi, modelElement, calculationName));
|
|
630
|
-
if (handledOperands.some(op => op === null)) { // Skip if any operand is invalid
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
return {[operator]: handledOperands};
|
|
634
|
-
} else {
|
|
635
|
-
logger.warn(`Invalid operands for operator '${operator}'. Expected an array. Skipping.`);
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
638
|
-
} else if (typeof calcExpression === 'string' && calcExpression.startsWith('$')) {
|
|
639
|
-
// This is a field reference
|
|
640
|
-
return handleOperand(calcExpression, fi, modelElement, calculationName);
|
|
641
|
-
} else {
|
|
642
|
-
// This is a constant value, return as is.
|
|
643
|
-
return calcExpression;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function handleOperand(operand, fi, modelElement, calculationName) {
|
|
648
|
-
if (typeof operand === 'object' && operand !== null && Object.keys(operand).length === 1) {
|
|
649
|
-
// Nested Calculation: recursively handle
|
|
650
|
-
return handleCalculationExpression(operand, fi, modelElement, calculationName);
|
|
651
|
-
} else if (typeof operand === 'string' && operand.startsWith('$')) {
|
|
652
|
-
// Field Reference: check field existence
|
|
653
|
-
const fieldName = operand.slice(1);
|
|
654
|
-
if (!isValidFieldReference(fieldName, modelElement)) {
|
|
655
|
-
logger.warn(`Invalid field reference '${fieldName}' in calculation. Skipping.`);
|
|
656
|
-
return null;
|
|
657
|
-
}
|
|
658
|
-
return operand;
|
|
659
|
-
} else {
|
|
660
|
-
// Constant Value
|
|
661
|
-
return operand;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function isValidFieldReference(fieldName, modelElement) {
|
|
666
|
-
// Check if the field exists in the model
|
|
667
|
-
return modelElement.fields.some(field => field.name === fieldName);
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function isValidAggregationOperator(operator) {
|
|
671
|
-
const arithmeticOperators = [
|
|
672
|
-
'$add', '$subtract', '$multiply', '$divide', '$mod', '$pow',
|
|
673
|
-
'$abs', '$ceil', '$floor', '$round', '$trunc', '$exp', '$log', '$log10'
|
|
674
|
-
];
|
|
675
|
-
const comparisonOperators = [
|
|
676
|
-
'$eq', '$gt', '$gte', '$lt', '$lte', '$ne'
|
|
677
|
-
// ... (others like $cmp, $strcasecmp, etc.)
|
|
678
|
-
];
|
|
679
|
-
const stringOperators = [
|
|
680
|
-
'$concat', '$strLenCP', '$substrCP', '$toLower', '$toUpper'
|
|
681
|
-
// ... (others)
|
|
682
|
-
];
|
|
683
|
-
const conditionalOperators = ['$cond', '$ifNull'];
|
|
684
|
-
// Add more categories and operators as needed
|
|
685
|
-
|
|
686
|
-
return [...arithmeticOperators, ...comparisonOperators, ...stringOperators, ...conditionalOperators].includes(operator);
|
|
1
|
+
import {getDefaultForType, getFieldValueHash} from "../../data.js";
|
|
2
|
+
import {Event} from "../../events.js";
|
|
3
|
+
import {getCollectionForUser, isObjectId} from "../mongodb.js";
|
|
4
|
+
import {ObjectId} from "mongodb";
|
|
5
|
+
import {isPlainObject} from "../../core.js";
|
|
6
|
+
import {dataTypes, getModel, searchData} from "./data.operations.js";
|
|
7
|
+
import {validateModelData} from "./data.validation.js";
|
|
8
|
+
import i18n from "../../i18n.js";
|
|
9
|
+
import {addFile, removeFile} from "../file.js";
|
|
10
|
+
import {Logger} from "../../gameObject.js";
|
|
11
|
+
import NodeCache from "node-cache";
|
|
12
|
+
|
|
13
|
+
let depthFilter = 0;
|
|
14
|
+
|
|
15
|
+
// Création du cache avec des options configurables
|
|
16
|
+
export const relationCache = new NodeCache({
|
|
17
|
+
stdTTL: 3600, // TTL par défaut de 1 heure (en secondes)
|
|
18
|
+
checkperiod: 600, // Vérification des éléments expirés toutes les 10 minutes
|
|
19
|
+
useClones: false // Pour des performances optimales avec des ObjectId
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
let engine, logger;
|
|
23
|
+
export function onInit(defaultEngine) {
|
|
24
|
+
engine = defaultEngine;
|
|
25
|
+
logger = engine.getComponent(Logger);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function convertDataTypes(dataArray, modelFields, sourceType = 'csv') {
|
|
29
|
+
return dataArray.map(record => {
|
|
30
|
+
const convertedRecord = {...record};
|
|
31
|
+
for (const field of modelFields) {
|
|
32
|
+
if (convertedRecord.hasOwnProperty(field.name)) {
|
|
33
|
+
let value = convertedRecord[field.name];
|
|
34
|
+
|
|
35
|
+
// Gérer les chaînes vides pour les champs non requis
|
|
36
|
+
if (typeof value === 'string' && value === '' && !field.required) {
|
|
37
|
+
convertedRecord[field.name] = getDefaultForType(field);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// Si la valeur est null ou undefined, on la laisse telle quelle, la validation s'en chargera
|
|
41
|
+
if (value === null || value === undefined) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
switch (field.type) {
|
|
46
|
+
case 'number':
|
|
47
|
+
if (typeof value !== 'number') { // Convertir si ce n'est pas déjà un nombre
|
|
48
|
+
const num = parseFloat(value);
|
|
49
|
+
if (!isNaN(num)) {
|
|
50
|
+
convertedRecord[field.name] = num;
|
|
51
|
+
} else {
|
|
52
|
+
logger.warn(`Import: Impossible de parser le nombre pour le champ ${field.name}, valeur: ${value}. Utilisation de la valeur par défaut/null.`);
|
|
53
|
+
convertedRecord[field.name] = getDefaultForType(field);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
case 'boolean':
|
|
58
|
+
if (typeof value !== 'boolean') {
|
|
59
|
+
convertedRecord[field.name] = ['true', '1', 'yes', 'on'].includes(String(value).toLowerCase());
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
case 'date':
|
|
63
|
+
case 'datetime':
|
|
64
|
+
if (String(value).toLowerCase() === 'now') {
|
|
65
|
+
convertedRecord[field.name] = 'now';
|
|
66
|
+
} else {
|
|
67
|
+
const parsedDate = new Date(value);
|
|
68
|
+
if (!isNaN(parsedDate.getTime())) {
|
|
69
|
+
convertedRecord[field.name] = field.type === 'date' ? parsedDate.toISOString().split("T")[0] : parsedDate.toISOString();
|
|
70
|
+
} else if (value) { // Ne pas logger si la valeur était initialement vide/null
|
|
71
|
+
logger.warn(`Import: Impossible de parser la date pour le champ ${field.name}, valeur: ${value}. La validation ulture s'en chargera.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
case 'array':
|
|
76
|
+
if (['csv', 'excel'].includes(sourceType) && typeof value === 'string') {
|
|
77
|
+
const arrayValues = value.split(/[,;]/).map(item => item.trim()).filter(item => item !== '');
|
|
78
|
+
if (field.itemsType === 'number') {
|
|
79
|
+
convertedRecord[field.name] = arrayValues.map(v => parseFloat(v)).filter(v => !isNaN(v));
|
|
80
|
+
} else {
|
|
81
|
+
convertedRecord[field.name] = arrayValues;
|
|
82
|
+
}
|
|
83
|
+
} else if (sourceType === 'json' && typeof value === 'string') {
|
|
84
|
+
try {
|
|
85
|
+
const parsedArray = JSON.parse(value);
|
|
86
|
+
if (Array.isArray(parsedArray)) {
|
|
87
|
+
convertedRecord[field.name] = parsedArray;
|
|
88
|
+
// TODO: Potentiellement convertir les éléments de parsedArray ici si nécessaire
|
|
89
|
+
} else {
|
|
90
|
+
logger.warn(`Import: La chaîne JSON pour le champ tableau ${field.name} n'a pas été parsée en tableau. Valeur: ${value}.`);
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
logger.warn(`Import: Impossible de parser la chaîne JSON pour le champ tableau ${field.name}. Valeur: ${value}.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Si c'est déjà un tableau (cas JSON typique), on suppose que les types des éléments sont corrects
|
|
97
|
+
// ou seront validés par pushDataUnsecure.
|
|
98
|
+
else if (!Array.isArray(convertedRecord[field.name])) {
|
|
99
|
+
convertedRecord[field.name] = getDefaultForType(field);
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case 'object':
|
|
103
|
+
if (['csv', 'excel'].includes(sourceType)) {
|
|
104
|
+
try {
|
|
105
|
+
convertedRecord[field.name] = JSON.parse(value);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
logger.warn(`Import: Impossible de parser la chaîne JSON pour le champ objet ${field.name}. Valeur: ${value}.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case 'code':
|
|
112
|
+
if (['csv', 'excel'].includes(sourceType) && typeof value === 'string') {
|
|
113
|
+
try {
|
|
114
|
+
convertedRecord[field.name] = JSON.parse(value);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
logger.warn(`Import: Impossible de parser la chaîne JSON pour le champ code (json) ${field.name}. Valeur: ${value}.`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return convertedRecord;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const removeValue = (obj, containsKey, removeParent = false) => {
|
|
128
|
+
// Base case: If the object is not an object or array, return it as is.
|
|
129
|
+
if (!isPlainObject(obj) && !Array.isArray(obj)) {
|
|
130
|
+
return obj;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const key in obj) {
|
|
134
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
135
|
+
if (removeParent) {
|
|
136
|
+
const value = obj[key]?.[containsKey];
|
|
137
|
+
if (value !== undefined) {
|
|
138
|
+
delete obj[key];
|
|
139
|
+
} else {
|
|
140
|
+
removeValue(obj[key], containsKey);
|
|
141
|
+
}
|
|
142
|
+
} else if (containsKey === key) {
|
|
143
|
+
delete obj[key];
|
|
144
|
+
} else {
|
|
145
|
+
removeValue(obj[key], containsKey);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return obj;
|
|
150
|
+
};
|
|
151
|
+
export const changeValue = (obj, keyToChange, changeFunction, excludeKeys = [], depth = 0, parentKey = '') => {
|
|
152
|
+
if (!depth) {
|
|
153
|
+
depthFilter = 0;
|
|
154
|
+
}
|
|
155
|
+
if (!isPlainObject(obj) && !Array.isArray(obj)) {
|
|
156
|
+
return obj;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const newObj = Array.isArray(obj) ? [] : {};
|
|
160
|
+
|
|
161
|
+
for (const key in obj) {
|
|
162
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
163
|
+
const topLevel = depthFilter === 0;
|
|
164
|
+
if (key === keyToChange) {
|
|
165
|
+
depthFilter++;
|
|
166
|
+
}
|
|
167
|
+
const value = obj[key];
|
|
168
|
+
if (value instanceof RegExp) {
|
|
169
|
+
newObj[key] = value;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (isPlainObject(value) && !excludeKeys.includes(key)) {
|
|
173
|
+
newObj[key] = changeValue(value, keyToChange, changeFunction, excludeKeys, depth + 1, key);
|
|
174
|
+
} else if (Array.isArray(value) && !excludeKeys.includes(key)) {
|
|
175
|
+
newObj[key] = value.map(item => {
|
|
176
|
+
if (isPlainObject(item)) {
|
|
177
|
+
return changeValue(item, keyToChange, changeFunction, excludeKeys, depth + 1, key);
|
|
178
|
+
}
|
|
179
|
+
return item;
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
newObj[key] = value;
|
|
183
|
+
}
|
|
184
|
+
if (key === keyToChange) {
|
|
185
|
+
if (typeof changeFunction === 'function') {
|
|
186
|
+
const newValue = changeFunction(parentKey, newObj[key], topLevel);
|
|
187
|
+
if (newValue !== undefined) {
|
|
188
|
+
if (isPlainObject(newValue)) {
|
|
189
|
+
return newValue;
|
|
190
|
+
} else {
|
|
191
|
+
newObj[key] = newValue;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
//delete newObj[key];
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return newObj;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export async function processDocuments(datas, model, collection, me) {
|
|
205
|
+
const idMap = new Map();
|
|
206
|
+
const allInsertedIds = [];
|
|
207
|
+
|
|
208
|
+
const realData = await Event.Trigger("OnDataInsert", "event", "system", datas) || datas;
|
|
209
|
+
for (const doc of realData) {
|
|
210
|
+
try {
|
|
211
|
+
const newDocId = await insertAndResolveRelations(doc, model, collection, me, idMap);
|
|
212
|
+
if (newDocId) {
|
|
213
|
+
allInsertedIds.push(newDocId.toString());
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
// Modification clé ici : on ne catch plus les erreurs de validation
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {allInsertedIds, idMap};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Traite toutes les relations du document
|
|
226
|
+
*/
|
|
227
|
+
export async function processRelations(docToProcess, model, collection, me, idMap) {
|
|
228
|
+
const batchFinds = [];
|
|
229
|
+
|
|
230
|
+
// Phase 1: Préparation des requêtes
|
|
231
|
+
for (const field of model.fields) {
|
|
232
|
+
if (field.type !== 'relation') continue;
|
|
233
|
+
|
|
234
|
+
const value = docToProcess[field.name];
|
|
235
|
+
if (value?.$find) {
|
|
236
|
+
batchFinds.push({
|
|
237
|
+
field: field.name,
|
|
238
|
+
promise: searchData({
|
|
239
|
+
filter: value.$find,
|
|
240
|
+
limit: field.multiple ? 0 : 1,
|
|
241
|
+
model: field.relation
|
|
242
|
+
}, me),
|
|
243
|
+
multiple: field.multiple
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Phase 2: Exécution parallèle
|
|
249
|
+
const findResults = await Promise.all(batchFinds.map(f => f.promise));
|
|
250
|
+
|
|
251
|
+
// Phase 3: Traitement des résultats
|
|
252
|
+
findResults.forEach((result, index) => {
|
|
253
|
+
const {field, multiple} = batchFinds[index];
|
|
254
|
+
if (result.data?.length > 0) {
|
|
255
|
+
// Cas où des documents sont trouvés
|
|
256
|
+
docToProcess[field] = multiple
|
|
257
|
+
? result.data.map(r => r._id.toString())
|
|
258
|
+
: result.data[0]._id.toString();
|
|
259
|
+
} else {
|
|
260
|
+
// Cas où AUCUN document n'est trouvé : il faut nettoyer le champ !
|
|
261
|
+
docToProcess[field] = multiple ? [] : null;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
for (const field of model.fields) {
|
|
267
|
+
if (field.type !== 'relation') continue;
|
|
268
|
+
|
|
269
|
+
const fieldName = field.name;
|
|
270
|
+
const relationValue = docToProcess[fieldName];
|
|
271
|
+
if (!relationValue || typeof relationValue !== 'object') continue;
|
|
272
|
+
|
|
273
|
+
const relatedModel = await getModel(field.relation, me);
|
|
274
|
+
|
|
275
|
+
if (!Array.isArray(relationValue) && relationValue['$find']) {
|
|
276
|
+
|
|
277
|
+
} else if (Array.isArray(relationValue)) {
|
|
278
|
+
// Relation multiple (tableau)
|
|
279
|
+
docToProcess[fieldName] = await processMultipleRelations(
|
|
280
|
+
relationValue,
|
|
281
|
+
relatedModel,
|
|
282
|
+
collection,
|
|
283
|
+
me,
|
|
284
|
+
idMap
|
|
285
|
+
);
|
|
286
|
+
} else if (isPlainObject(relationValue)) {
|
|
287
|
+
// Relation simple (objet)
|
|
288
|
+
docToProcess[fieldName] = await processSingleRelation(
|
|
289
|
+
relationValue,
|
|
290
|
+
relatedModel,
|
|
291
|
+
collection,
|
|
292
|
+
me,
|
|
293
|
+
idMap
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Traite une relation multiple (tableau)
|
|
301
|
+
*/
|
|
302
|
+
async function processMultipleRelations(items, relatedModel, collection, me, idMap) {
|
|
303
|
+
const newRelationIds = await Promise.all(
|
|
304
|
+
items.map(item => processRelationItem(item, relatedModel, collection, me, idMap))
|
|
305
|
+
);
|
|
306
|
+
return newRelationIds.filter(id => id).map(id => id.toString());
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Traite une relation simple (objet)
|
|
311
|
+
*/
|
|
312
|
+
async function processSingleRelation(item, relatedModel, collection, me, idMap) {
|
|
313
|
+
const newId = await processRelationItem(item, relatedModel, collection, me, idMap);
|
|
314
|
+
return newId ? newId.toString() : null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function processRelationItem(item, relatedModel, collection, me, idMap) {
|
|
318
|
+
// Cas 1: ID existant (string ou ObjectId)
|
|
319
|
+
if (isObjectId(item) || typeof item === 'string') {
|
|
320
|
+
const originalId = typeof item === 'string' ? item : item.toString();
|
|
321
|
+
|
|
322
|
+
// Vérifier si cet ID a déjà été mappé (cas d'une référence circulaire)
|
|
323
|
+
if (idMap.has(originalId)) {
|
|
324
|
+
return idMap.get(originalId);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Sinon, vérifier si l'ID existe en base
|
|
328
|
+
const existing = await collection.findOne({
|
|
329
|
+
_id: new ObjectId(originalId),
|
|
330
|
+
_model: relatedModel.name,
|
|
331
|
+
$or: [{_user: me._user || me.username}, {_user: {$exists: false}}]
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (existing) {
|
|
335
|
+
return existing._id; // Conserver l'ID original
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Cas 2: Objet complet à importer
|
|
340
|
+
if (isPlainObject(item)) {
|
|
341
|
+
const relationDoc = prepareDocument(item, relatedModel, me);
|
|
342
|
+
applyDefaultValues(relationDoc, relatedModel);
|
|
343
|
+
|
|
344
|
+
// Si l'objet a un _id, essayer de le conserver
|
|
345
|
+
if (item._id) {
|
|
346
|
+
const originalId = item._id.toString();
|
|
347
|
+
|
|
348
|
+
// Vérifier si l'ID existe déjà en base
|
|
349
|
+
const existing = await collection.findOne({
|
|
350
|
+
_id: new ObjectId(originalId),
|
|
351
|
+
_model: relatedModel.name,
|
|
352
|
+
$or: [{_user: me._user || me.username}, {_user: {$exists: false}}]
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
if (existing) {
|
|
356
|
+
return existing._id; // Utiliser l'ID existant
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Si l'ID n'existe pas encore, l'utiliser pour le nouvel insert
|
|
360
|
+
relationDoc._id = new ObjectId(originalId);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const relationHash = relationDoc._hash;
|
|
364
|
+
const cacheKey = `${relatedModel.name}:${relationHash}`;
|
|
365
|
+
|
|
366
|
+
// Vérification dans le cache
|
|
367
|
+
const cachedId = relationCache.get(cacheKey);
|
|
368
|
+
if (cachedId !== undefined) {
|
|
369
|
+
return cachedId;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Vérification en base de données par hash
|
|
373
|
+
const existingByHash = await collection.findOne({
|
|
374
|
+
_hash: relationHash,
|
|
375
|
+
_model: relatedModel.name,
|
|
376
|
+
_user: relationDoc._user
|
|
377
|
+
}, {projection: {_id: 1}});
|
|
378
|
+
|
|
379
|
+
if (existingByHash) {
|
|
380
|
+
relationCache.set(cacheKey, existingByHash._id);
|
|
381
|
+
return existingByHash._id;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const newId = await insertAndResolveRelations(item, relatedModel, collection, me, idMap);
|
|
385
|
+
relationCache.set(cacheKey, newId);
|
|
386
|
+
return newId;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Fonction pour vider le cache si besoin
|
|
393
|
+
function clearRelationCache() {
|
|
394
|
+
relationCache.flushAll();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Applique les filtres de champ définis dans le modèle
|
|
399
|
+
*/
|
|
400
|
+
async function applyFieldFilters(docToProcess, model) {
|
|
401
|
+
for (const field of model.fields) {
|
|
402
|
+
docToProcess[field.name] = typeof (docToProcess[field.name]) === 'undefined' || docToProcess[field.name] === null ? field.default : docToProcess[field.name];
|
|
403
|
+
if (dataTypes[field.type]?.filter) {
|
|
404
|
+
const filter = await dataTypes[field.type].filter(
|
|
405
|
+
docToProcess[field.name],
|
|
406
|
+
field
|
|
407
|
+
);
|
|
408
|
+
const realFilter = await Event.Trigger('OnDataFilter', "event", "system", filter, field, docToProcess);
|
|
409
|
+
docToProcess[field.name] = realFilter || filter;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Applique les valeurs par défaut aux champs manquants
|
|
416
|
+
*/
|
|
417
|
+
function applyDefaultValues(doc, model) {
|
|
418
|
+
for (const field of model.fields) {
|
|
419
|
+
// Si le champ n'est pas défini et a une valeur par défaut
|
|
420
|
+
if (!(field.name in doc) && 'default' in field) {
|
|
421
|
+
doc[field.name] = typeof field.default === 'function'
|
|
422
|
+
? field.default()
|
|
423
|
+
: field.default;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function insertAndResolveRelations(doc, model, collection, me, idMap) {
|
|
429
|
+
const originalId = doc._id?.toString();
|
|
430
|
+
|
|
431
|
+
// Si cet ID a déjà été traité, retourner le nouvel ID mappé
|
|
432
|
+
if (originalId && idMap.has(originalId)) {
|
|
433
|
+
return idMap.get(originalId);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const docToProcess = prepareDocument(doc, model, me);
|
|
437
|
+
applyDefaultValues(docToProcess, model);
|
|
438
|
+
|
|
439
|
+
// Si le document a un _id original et qu'il n'existe pas encore, le conserver
|
|
440
|
+
if (originalId && !await collection.findOne({_id: new ObjectId(originalId)})) {
|
|
441
|
+
docToProcess._id = new ObjectId(originalId);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await validateModelData(docToProcess, model);
|
|
445
|
+
await processRelations(docToProcess, model, collection, me, idMap);
|
|
446
|
+
await validateModelData(docToProcess, model);
|
|
447
|
+
await applyFieldFilters(docToProcess, model);
|
|
448
|
+
await checkUniqueFields(docToProcess, model, collection);
|
|
449
|
+
|
|
450
|
+
const existingDoc = await findExistingDocument(docToProcess, collection);
|
|
451
|
+
if (existingDoc) {
|
|
452
|
+
cacheDocumentId(originalId, existingDoc._id, idMap);
|
|
453
|
+
return existingDoc._id;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
for (const field of model.fields) {
|
|
457
|
+
if (field.type === 'relation' && field.relationFilter && docToProcess[field.name]) {
|
|
458
|
+
|
|
459
|
+
const relatedIds = Array.isArray(docToProcess[field.name])
|
|
460
|
+
? docToProcess[field.name]
|
|
461
|
+
: [docToProcess[field.name]];
|
|
462
|
+
|
|
463
|
+
// Préparer un filtre global : match si _id dans relatedIds ET respecte relationFilter
|
|
464
|
+
const validationQuery = {
|
|
465
|
+
$and: [
|
|
466
|
+
{$in: ['$_id', relatedIds.map(id => ({$toObjectId: id}))]},
|
|
467
|
+
field.relationFilter
|
|
468
|
+
]
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const relatedDocs = await searchData({
|
|
472
|
+
filter: validationQuery,
|
|
473
|
+
model: field.relation,
|
|
474
|
+
limit: relatedIds.length
|
|
475
|
+
}, me);
|
|
476
|
+
|
|
477
|
+
if ((relatedDocs?.count || 0) !== relatedIds.length) {
|
|
478
|
+
const invalidIds = relatedIds.filter(id =>
|
|
479
|
+
!relatedDocs.data.some(doc => doc._id.toString() === id.toString())
|
|
480
|
+
);
|
|
481
|
+
throw new Error(
|
|
482
|
+
i18n.t(
|
|
483
|
+
'api.data.relationFilterFailed',
|
|
484
|
+
'Les valeurs {{values}} pour le champ {{field}} ne respectent pas le filtre de relation défini.',
|
|
485
|
+
{field: field.name, values: invalidIds.join(', ')}
|
|
486
|
+
)
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
// Insertion en conservant éventuellement l'ID original
|
|
494
|
+
const result = docToProcess._id
|
|
495
|
+
? await collection.insertOne(docToProcess)
|
|
496
|
+
: await collection.insertOne(docToProcess);
|
|
497
|
+
|
|
498
|
+
const insertedId = result.insertedId;
|
|
499
|
+
cacheDocumentId(originalId, insertedId, idMap);
|
|
500
|
+
|
|
501
|
+
return insertedId;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Nouvelle fonction pour vérifier les champs uniques
|
|
505
|
+
async function checkUniqueFields(doc, model, collection) {
|
|
506
|
+
const uniqueFields = model.fields.filter(f => f.unique);
|
|
507
|
+
|
|
508
|
+
for (const field of uniqueFields) {
|
|
509
|
+
const value = doc[field.name];
|
|
510
|
+
if (value === undefined || value === null) continue;
|
|
511
|
+
|
|
512
|
+
const existing = await collection.findOne({
|
|
513
|
+
[field.name]: value,
|
|
514
|
+
_model: model.name,
|
|
515
|
+
_user: doc._user
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
if (existing) {
|
|
519
|
+
// Utilisation de i18n pour un message d'erreur standardisé
|
|
520
|
+
throw new Error(i18n.t('api.data.duplicateValue', {field: field.name, value: value}));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function prepareDocument(doc, model, me) {
|
|
526
|
+
const docToProcess = {...doc};
|
|
527
|
+
delete docToProcess._id;
|
|
528
|
+
|
|
529
|
+
// AJOUT: Nettoyage des champs non définis dans le modèle
|
|
530
|
+
for (const key of Object.keys(docToProcess)) {
|
|
531
|
+
if (!model.fields.some(f => f.name === key) && !key.startsWith('_')) {
|
|
532
|
+
delete docToProcess[key];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
docToProcess._model = model.name;
|
|
537
|
+
docToProcess._user = me._user || me.username;
|
|
538
|
+
docToProcess._hash = getFieldValueHash(model, docToProcess);
|
|
539
|
+
|
|
540
|
+
return docToProcess;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Cherche un document existant par son hash
|
|
545
|
+
*/
|
|
546
|
+
async function findExistingDocument(docToProcess, collection) {
|
|
547
|
+
return await collection.findOne({
|
|
548
|
+
_hash: docToProcess._hash,
|
|
549
|
+
_model: docToProcess._model,
|
|
550
|
+
_user: docToProcess._user
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Insère le document dans la collection
|
|
556
|
+
*/
|
|
557
|
+
async function insertDocument(docToProcess, collection) {
|
|
558
|
+
const result = await collection.insertOne(docToProcess);
|
|
559
|
+
return result.insertedId;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Met en cache la correspondance d'ID
|
|
564
|
+
*/
|
|
565
|
+
function cacheDocumentId(originalId, newId, idMap) {
|
|
566
|
+
if (originalId && newId) {
|
|
567
|
+
idMap.set(originalId, newId);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Gestion des fichiers (à implémenter selon besoins)
|
|
573
|
+
*/
|
|
574
|
+
export async function handleFilesIfNeeded(insertedIds, files, model, collection) {
|
|
575
|
+
// Implémentation spécifique à votre application
|
|
576
|
+
// Ex: association des fichiers uploadés aux documents insérés
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export const checkHash = async (me, model, hash, excludeId = null) => {
|
|
580
|
+
const collection = await getCollectionForUser(me);
|
|
581
|
+
const query = {
|
|
582
|
+
_model: model.name,
|
|
583
|
+
_hash: hash,
|
|
584
|
+
...(excludeId && {_id: {$ne: new ObjectId(excludeId)}})
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
console.log("Query being executed:", JSON.stringify(query, null, 2));
|
|
588
|
+
|
|
589
|
+
const count = await collection.countDocuments(query);
|
|
590
|
+
return count > 0;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// Fonctions helper
|
|
594
|
+
export async function processFileArray(files, currentFiles, user) {
|
|
595
|
+
const newFiles = await Promise.allSettled(
|
|
596
|
+
Object.keys(files).map(f => files[f]).map(async (file, i) => {
|
|
597
|
+
const oldFile = currentFiles.find(f => f.name === file.name);
|
|
598
|
+
if (oldFile && !file.newFile) return oldFile;
|
|
599
|
+
if (file.guid) return file;
|
|
600
|
+
if (!file.newFile) return Promise.reject();
|
|
601
|
+
return await addFile(files[i], user);
|
|
602
|
+
})
|
|
603
|
+
).then(results => results.map(r => r.value).filter(Boolean));
|
|
604
|
+
|
|
605
|
+
// Suppression des anciens fichiers non réutilisés
|
|
606
|
+
await Promise.allSettled(
|
|
607
|
+
currentFiles
|
|
608
|
+
.filter(f => !newFiles.some(nf => nf._id === f._id))
|
|
609
|
+
.map(f => removeFile(f, user))
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
return newFiles;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function handleCalculationExpression(calcExpression, fi, modelElement, calculationName) {
|
|
616
|
+
// Check if the calculation expression involves an operator
|
|
617
|
+
if (typeof calcExpression === 'object' && calcExpression !== null && Object.keys(calcExpression).length === 1) {
|
|
618
|
+
const operator = Object.keys(calcExpression)[0];
|
|
619
|
+
const operands = calcExpression[operator];
|
|
620
|
+
|
|
621
|
+
// Validation of isValidAggregationOperator
|
|
622
|
+
if (!isValidAggregationOperator(operator)) {
|
|
623
|
+
logger.warn(`Invalid aggregation operator '${operator}' in calculation. Skipping.`);
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Check Operand Count and Apply $ifNull handling
|
|
628
|
+
if (Array.isArray(operands)) {
|
|
629
|
+
const handledOperands = operands.map(operand => handleOperand(operand, fi, modelElement, calculationName));
|
|
630
|
+
if (handledOperands.some(op => op === null)) { // Skip if any operand is invalid
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
return {[operator]: handledOperands};
|
|
634
|
+
} else {
|
|
635
|
+
logger.warn(`Invalid operands for operator '${operator}'. Expected an array. Skipping.`);
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
} else if (typeof calcExpression === 'string' && calcExpression.startsWith('$')) {
|
|
639
|
+
// This is a field reference
|
|
640
|
+
return handleOperand(calcExpression, fi, modelElement, calculationName);
|
|
641
|
+
} else {
|
|
642
|
+
// This is a constant value, return as is.
|
|
643
|
+
return calcExpression;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function handleOperand(operand, fi, modelElement, calculationName) {
|
|
648
|
+
if (typeof operand === 'object' && operand !== null && Object.keys(operand).length === 1) {
|
|
649
|
+
// Nested Calculation: recursively handle
|
|
650
|
+
return handleCalculationExpression(operand, fi, modelElement, calculationName);
|
|
651
|
+
} else if (typeof operand === 'string' && operand.startsWith('$')) {
|
|
652
|
+
// Field Reference: check field existence
|
|
653
|
+
const fieldName = operand.slice(1);
|
|
654
|
+
if (!isValidFieldReference(fieldName, modelElement)) {
|
|
655
|
+
logger.warn(`Invalid field reference '${fieldName}' in calculation. Skipping.`);
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
return operand;
|
|
659
|
+
} else {
|
|
660
|
+
// Constant Value
|
|
661
|
+
return operand;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function isValidFieldReference(fieldName, modelElement) {
|
|
666
|
+
// Check if the field exists in the model
|
|
667
|
+
return modelElement.fields.some(field => field.name === fieldName);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function isValidAggregationOperator(operator) {
|
|
671
|
+
const arithmeticOperators = [
|
|
672
|
+
'$add', '$subtract', '$multiply', '$divide', '$mod', '$pow',
|
|
673
|
+
'$abs', '$ceil', '$floor', '$round', '$trunc', '$exp', '$log', '$log10'
|
|
674
|
+
];
|
|
675
|
+
const comparisonOperators = [
|
|
676
|
+
'$eq', '$gt', '$gte', '$lt', '$lte', '$ne'
|
|
677
|
+
// ... (others like $cmp, $strcasecmp, etc.)
|
|
678
|
+
];
|
|
679
|
+
const stringOperators = [
|
|
680
|
+
'$concat', '$strLenCP', '$substrCP', '$toLower', '$toUpper'
|
|
681
|
+
// ... (others)
|
|
682
|
+
];
|
|
683
|
+
const conditionalOperators = ['$cond', '$ifNull'];
|
|
684
|
+
// Add more categories and operators as needed
|
|
685
|
+
|
|
686
|
+
return [...arithmeticOperators, ...comparisonOperators, ...stringOperators, ...conditionalOperators].includes(operator);
|
|
687
687
|
}
|